(function (context, $) {

  "use strict";

  context.JK = context.JK || {};
  context.JK.GearTest = function (app) {

    var logger = context.JK.logger;

    var isAutomated = false;
    var drawUI = false;
    var scoring = false;
    var validLatencyScore = false;
    var validIOScore = false;
    var latencyScore = null;
    var ioScore = null;
    var lastSavedTime = new Date();
    // this should be marked TRUE when the backend sends an invalid_audio_device alert
    var asynchronousInvalidDevice = false;
    var selectedDeviceInfo = null;


    var $scoreReport = null;
    var $ioHeader = null;
    var $latencyHeader = null;
    var $ioRate = null;
    var $ioRateScore = null;
    var $ioVar = null;
    var $ioVarScore = null;
    var $ioCountdown = null;
    var $ioCountdownSecs = null;
    var $latencyScore = null;
    var $resultsText = null;
    var $unknownText = null;
    var $loopbackCompleted = null;
    var $adjustGearSpeedCompleted = null;
    var $adjustGearForIoFail = null;
    var $ioScoreSection = null;
    var $latencyScoreSection = null;


    var $self = $(this);

    var GEAR_TEST_START = "gear_test.start";
    var GEAR_TEST_IO_START = "gear_test.io_start";
    var GEAR_TEST_IO_DONE = "gear_test.io_done";
    var GEAR_TEST_LATENCY_START = "gear_test.latency_start";
    var GEAR_TEST_LATENCY_DONE = "gear_test.latency_done";
    var GEAR_TEST_DONE = "gear_test.done";
    var GEAR_TEST_FAIL = "gear_test.fail";
    var GEAR_TEST_IO_PROGRESS = "gear_test.io_progress";
    var GEAR_TEST_INVALIDATED_ASYNC = "gear_test.async_invalidated"; // happens when backend alerts us device is invalid


    function isWdm() {
      return selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')
    }

    function isGoodFtue() {
      return (isWdm() || validIOScore) && !asynchronousInvalidDevice;
    }

    function processIOScore(io) {
      // take the higher variance, which is apparently actually std dev
      var std = io.in_var > io.out_var ? io.in_var : io.out_var;
      std = Math.round(std * 100) / 100;
      // take the furthest-off-from-target io rate
      var median = Math.abs(io.in_median - io.in_target) > Math.abs(io.out_median - io.out_target) ? [io.in_median, io.in_target] : [io.out_median, io.out_target];
      var medianTarget = median[1];
      median = Math.round(median[0]);

      console.log("io", io, std, median)
      var stdIOClass = 'bad';
      if (std <= 0.50) {
        stdIOClass = 'good';
      }
      else if (std <= 1.00) {
        stdIOClass = 'acceptable';
      }

      var medianIOClass = 'bad';
      if (Math.abs(median - medianTarget) <= ((io.in_target / 100) * .5)) {
        medianIOClass = 'good';
      }
      else if (Math.abs(median - medianTarget) <= ((io.in_target / 100))) {
        medianIOClass = 'acceptable';
      }

      // uncomment one to force a particular type of I/O failure
      // medianIOClass = "bad";
      // stdIOClass = "bad"

      // take worst between median or std
      var ioClassToNumber = {bad: 2, acceptable: 1, good: 0}
      var aggregrateIOClass = ioClassToNumber[stdIOClass] > ioClassToNumber[medianIOClass] ? stdIOClass : medianIOClass;

      // now base the overall IO score based on both values.

      $self.triggerHandler(GEAR_TEST_IO_DONE, {std:std, median:median, io:io, aggregrateIOClass: aggregrateIOClass, medianIOClass : medianIOClass, stdIOClass: stdIOClass, validLatencyScore: validLatencyScore})
      //renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass);

      if(aggregrateIOClass == "bad") {
        validIOScore = false;
      }
      else {
        validIOScore = true;
      }

      scoring = false;

      if(isGoodFtue()) {
        $self.triggerHandler(GEAR_TEST_DONE, {validLatencyScore: validLatencyScore})
      }
      else {
        $self.triggerHandler(GEAR_TEST_FAIL, {reason:'io', ioTarget: medianIOClass, ioTargetScore: median,  ioVariance: stdIOClass, ioVarianceScore: std, validLatencyScore: validLatencyScore});
      }
    }

    function automaticScore() {
      logger.debug("automaticScore: calling FTUESave(false)");
      lastSavedTime = new Date(); // save before and after FTUESave, because the event happens in a multithreaded way
      var result = context.jamClient.FTUESave(false);
      lastSavedTime = new Date();
      if(result && result.error) {
        logger.debug("unable to FTUESave(false). reason=" + result.detail);
        context.JK.GearTest.testDeferred.reject(result);
        return false;
      }
      else {
        var latency = context.jamClient.FTUEGetExpectedLatency();
        context.JK.GearTest.testDeferred.resolve(latency);
      }

      return true;
    }

    function loopbackScore() {
      var cbFunc = 'JK.loopbackLatencyCallback';
      logger.debug("Registering loopback latency callback: " + cbFunc);
      context.jamClient.FTUERegisterLatencyCallback('JK.loopbackLatencyCallback');
      var now = new Date();
      logger.debug("Starting Latency Test..." + now);
      context.jamClient.FTUEStartLatency();
    }

    // refocused affects how IO testing occurs.
    // on refocus=true:
    //  * reuse IO score if it was good/acceptable
    //  * rescore IO if it was bad or skipped from previous try
    function attemptScore(_selectedDeviceInfo, refocused) {
      if(scoring) {
        logger.debug("gear-test: already scoring");
        return;
      }
      selectedDeviceInfo = _selectedDeviceInfo;
      scoring = true;
      asynchronousInvalidDevice = false;
      $self.triggerHandler(GEAR_TEST_START);
      $self.triggerHandler(GEAR_TEST_LATENCY_START);
      validLatencyScore = false;
      latencyScore = null;
      if(!refocused) {
        // don't reset a valid IO score on refocus
        validIOScore = false;
        ioScore = null;
      }

      // this timer exists to give UI time to update for renderScoringStarted before blocking nature of jamClient.FTUESave(save) kicks in
      setTimeout(function () {

        context.JK.GearTest.testDeferred = new $.Deferred();
        if(isAutomated) {
          // use automated latency score mechanism
          automaticScore()
        }
        else {
          // use loopback score mechanism
          loopbackScore();
        }

        context.JK.GearTest.testDeferred.done(function(latency) {
          latencyScore = latency;

          if(isAutomated) {
            // uncomment to do a manual loopback test
            //latency.latencyknown = false;
          }

          updateScoreReport(latency, refocused);

          if (true || validLatencyScore) {
            $self.triggerHandler(GEAR_TEST_IO_START);
            // reuse valid IO score if this is on refocus
            if(false && (refocused && validIOScore)) {
              processIOScore(ioScore);
            }
            else {
              var testTimeSeconds = gon.ftue_io_wait_time; // allow time for IO to establish itself
              var startTime = testTimeSeconds / 2; // start measuring half way through the test, to get past IO oddities
              $self.trigger(GEAR_TEST_IO_PROGRESS, {countdown:testTimeSeconds, first:true})

              var interval = setInterval(function () {
                testTimeSeconds -= 1;
                $self.trigger(GEAR_TEST_IO_PROGRESS, {countdown:testTimeSeconds, first:false})

                if(testTimeSeconds == startTime) {
                  logger.debug("Starting IO Perf Test starting at " + startTime + "s in")
                  context.jamClient.FTUEStartIoPerfTest();
                }

                if (testTimeSeconds == 0) {
                  clearInterval(interval);
                  logger.debug("Ending IO Perf Test at " + testTimeSeconds + "s in")
                  var io = context.jamClient.FTUEGetIoPerfData();
                  ioScore = io;
                  processIOScore(io);
                }
              }, 1000);
            }
          }
          else {
            scoring = false;
            $self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency', validLatencyScore: validLatencyScore, latencyScore: latencyScore.latency})
          }
        })
        .fail(function(ftueSaveResult) {
            scoring = false;
            $self.triggerHandler(GEAR_TEST_FAIL, {reason:'invalid_configuration', validLatencyScore: validLatencyScore, data: ftueSaveResult})
        })
      }, 250);
    }

    function updateScoreReport(latencyResult, refocused) {
      var latencyClass = "neutral";
      var latencyValue = null;
      var validLatency = false;
      console.log("FTUEGetExpectedLatency", latencyResult)
      if (latencyResult && latencyResult.latencyknown) {
        var latencyValue = latencyResult.latency;
        latencyValue = Math.round(latencyValue * 100) / 100;
        if (latencyValue <= 10) {
          latencyClass = "good";
          validLatency = true;
        } else if (latencyValue <= gon.ftue_maximum_gear_latency) {
          latencyClass = "acceptable";
          validLatency = true;
        } else {
          latencyClass = "bad";
        }
      }
      else {
        latencyClass = 'unknown';
      }

      // uncomment these two lines to fail test due to latency
      // latencyClass = "bad";
      // validLatency = false;

      validLatencyScore = validLatency;

      if(refocused) {
        context.JK.prodBubble($scoreReport, 'refocus-rescore', {validIOScore: validIOScore}, {positions:['top', 'left']});
      }

      $self.triggerHandler(GEAR_TEST_LATENCY_DONE, {latencyValue: latencyValue, latencyClass: latencyClass, refocused: refocused});
    }

    // std deviation is the worst value between in/out
    // media is the worst value between in/out
    // io is the value returned by the backend, which has more info
    // ioClass is the pre-computed rollup class describing the result in simple terms of 'good', 'acceptable', bad'
    function renderIOScore(std, median, ioData, ioClass, ioRateClass, ioVarClass) {
      $ioRateScore.text(median !== null ? median : '');
      $ioVarScore.text(std !== null ? std : '');
      if (ioClass && ioClass != "starting" && ioClass != "skip") {
        $ioRate.show();
        $ioVar.show();
      }
      if(ioClass == 'starting' || ioClass == 'skip') {
        $ioHeader.show();
      }
      $resultsText.attr('io-rate-score', ioRateClass);
      $resultsText.attr('io-var-score', ioVarClass);
      $ioScoreSection.removeClass('good acceptable bad unknown starting skip')
      if (ioClass) {
        $ioScoreSection.addClass(ioClass);
      }
      // TODO: show help bubble of all data in IO data
    }

    function isScoring() {
      return scoring;
    }

    function isValidLatencyScore() {
      return validLatencyScore;
    }

    function isValidIOScore() {
      return validIOScore;
    }

    function getLatencyScore() {
      return latencyScore;
    }

    function getIOScore() {
      return ioScore;
    }

    function getLastSavedTime() {
      return lastSavedTime;
    }

    function onInvalidAudioDevice() {
      logger.debug("gear_test: onInvalidAudioDevice")
      asynchronousInvalidDevice = true;
      $self.triggerHandler(GEAR_TEST_INVALIDATED_ASYNC);
      context.JK.Banner.showAlert('Invalid Audio Device', 'It appears this audio device is not currently connected. Attach the device to your computer and restart the application, or select a different device.<br/><br/>If you think your gear is connected and working:<br/><br/>Try a different sample rate')

    }

    function showLoopbackDone() {
      $loopbackCompleted.show();
    }

    function showGearAdjustmentDone() {
      $adjustGearSpeedCompleted.show();
    }

    function resetScoreReport() {
      $ioHeader.hide();
      $latencyHeader.hide();
      $ioRate.hide();
      $ioRateScore.empty();
      $ioVar.hide();
      $ioVarScore.empty();
      $latencyScore.empty();
      $resultsText.removeAttr('latency-score');
      $resultsText.removeAttr('io-var-score');
      $resultsText.removeAttr('io-rate-score');
      $resultsText.removeAttr('scored');
      $unknownText.hide();
      $loopbackCompleted.hide();
      $adjustGearSpeedCompleted.hide();
      $ioScoreSection.removeClass('good acceptable bad unknown starting skip');
      $latencyScoreSection.removeClass('good acceptable bad unknown starting')
    }

    function invalidateScore() {
      validLatencyScore = false;
      validIOScore = false;
      asynchronousInvalidDevice = false;
      resetScoreReport();
    }

    function renderLatencyScore(latencyValue, latencyClass) {
      // latencyValue == null implies starting condition
      if (latencyValue) {
        $latencyScore.text(latencyValue + ' ms');
      }
      else {
        $latencyScore.text('');
      }


      if(latencyClass == 'unknown') {
        $latencyScore.text('Unknown');
        $unknownText.show();
      }

      $latencyHeader.show();
      $resultsText.attr('latency-score', latencyClass);
      $latencyScoreSection.removeClass('good acceptable bad unknown starting').addClass(latencyClass);
    }

    function renderIOScoringStarted(secondsLeft) {
      $ioCountdownSecs.text(secondsLeft);
      $ioCountdown.show();
    }

    function renderIOScoringStopped() {
      $ioCountdown.hide();
    }

    function renderIOCountdown(secondsLeft) {
      $ioCountdownSecs.text(secondsLeft);
    }

    function uniqueDeviceName() {
      try {
        return selectedDeviceInfo.input.info.displayName + '(' + selectedDeviceInfo.input.behavior.shortName + ')' + '-' +
          selectedDeviceInfo.output.info.displayName + '(' + selectedDeviceInfo.output.behavior.shortName + ')' + '-' +
          context.JK.GetOSAsString();
      }
      catch(e){
        logger.error("unable to devise unique device name for stats: " + e.toString());
        return "Unknown";
      }
    }

    function handleUI($testResults) {

      if(!$testResults.is('.ftue-box.results')) {
        throw "GearTest != .ftue-box.results"
      }

      $scoreReport = $testResults;
      $ioHeader = $scoreReport.find('.io');;
      $latencyHeader = $scoreReport.find('.latency');
      $ioRate = $scoreReport.find('.io-rate');
      $ioRateScore = $scoreReport.find('.io-rate-score');
      $ioVar = $scoreReport.find('.io-var');
      $ioVarScore = $scoreReport.find('.io-var-score');
      $ioScoreSection = $scoreReport.find('.io-score-section');
      $latencyScore = $scoreReport.find('.latency-score');
      $ioCountdown = $scoreReport.find('.io-countdown');
      $ioCountdownSecs = $scoreReport.find('.io-countdown .secs');
      $resultsText = $scoreReport.find('.results-text');
      $unknownText = $scoreReport.find('.unknown-text');
      $loopbackCompleted = $scoreReport.find('.loopback-completed')
      $adjustGearSpeedCompleted = $scoreReport.find('.adjust-gear-speed-completed');
      $adjustGearForIoFail = $scoreReport.find(".adjust-gear-for-io-fail")
      $latencyScoreSection = $scoreReport.find('.latency-score-section');

      function onGearTestStart(e, data) {
        renderIOScore(null, null, null, null, null, null);
      }

      function onGearTestIOStart(e, data) {
        renderIOScore(null, null, null, 'starting', 'starting', 'starting');
      }

      function onGearTestLatencyStart(e, data) {
        resetScoreReport();
        renderLatencyScore(null, 'starting');
      }

      function onGearTestLatencyDone(e, data) {
        renderLatencyScore(data.latencyValue, data.latencyClass);
      }

      function onGearTestIOProgress(e, data) {
        if(data.first) {
          renderIOScoringStarted(data.countdown);
        }

        renderIOCountdown(data.countdown);

        if(data.countdown == 0) {
          renderIOScoringStopped();
        }
      }

      function onGearTestIODone(e, data) {
        renderIOScore(data.std, data.median, data.io, data.aggregrateIOClass, data.medianIOClass, data.stdIOClass);
      }

      function onGearTestDone(e, data) {
        $resultsText.attr('scored', 'complete');

        context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.pass, latencyScore);

        rest.userCertifiedGear({success: true, client_id: app.clientId, audio_latency: getLatencyScore().latency});
      }

      function onGearTestFail(e, data) {
        $resultsText.attr('scored', 'complete');

        if(data.reason == "latency") {
          renderIOScore(null, null, null, 'skip', 'skip', 'skip');
        }

        rest.userCertifiedGear({success: false, client_id: app.clientId});

        if(data.reason == "latency") {
          context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.latencyFail, data.latencyScore);
        }
        else if(data.reason = "io") {
          if(data.ioTarget == 'bad') {
            context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.ioTargetFail, data.ioTargetScore);
          }
          else {
            context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.ioVarianceFail, data.ioVarianceScore);
          }
        }
        else if(data.reason == 'invalid_configuration') {
          logger.error("invalid configuration returned by gearTest." + data.data.detail)
        }
        else {
          logger.error("unknown reason in onGearTestFail: " + data.reason)
        }
      }

      $self
        .on(GEAR_TEST_START, onGearTestStart)
        .on(GEAR_TEST_IO_START, onGearTestIOStart)
        .on(GEAR_TEST_LATENCY_START, onGearTestLatencyStart)
        .on(GEAR_TEST_LATENCY_DONE, onGearTestLatencyDone)
        .on(GEAR_TEST_IO_PROGRESS, onGearTestIOProgress)
        .on(GEAR_TEST_IO_DONE, onGearTestIODone)
        .on(GEAR_TEST_DONE, onGearTestDone)
        .on(GEAR_TEST_FAIL, onGearTestFail);
    }

    function initialize($testResults, automated, noUI) {

      isAutomated = automated;
      drawUI = !noUI;

      if(drawUI) {
        handleUI($testResults);
      }
    }

    // Latency Test Callback
    context.JK.loopbackLatencyCallback = function (latencyMS) {
      // Unregister callback:
      context.jamClient.FTUERegisterLatencyCallback('');

      logger.debug("loopback test done: " + latencyMS);
      context.JK.GearTest.testDeferred.resolve({latency: latencyMS, latencyknown:true})
    };

    this.GEAR_TEST_START = GEAR_TEST_START;
    this.GEAR_TEST_IO_START = GEAR_TEST_IO_START;
    this.GEAR_TEST_IO_DONE = GEAR_TEST_IO_DONE;
    this.GEAR_TEST_LATENCY_START = GEAR_TEST_LATENCY_START;
    this.GEAR_TEST_LATENCY_DONE = GEAR_TEST_LATENCY_DONE;
    this.GEAR_TEST_DONE = GEAR_TEST_DONE;
    this.GEAR_TEST_FAIL = GEAR_TEST_FAIL;
    this.GEAR_TEST_IO_PROGRESS = GEAR_TEST_IO_PROGRESS;
    this.GEAR_TEST_INVALIDATED_ASYNC = GEAR_TEST_INVALIDATED_ASYNC;

    this.initialize           = initialize;
    this.isScoring            = isScoring;
    this.attemptScore         = attemptScore;
    this.resetScoreReport     = resetScoreReport;
    this.showLoopbackDone     = showLoopbackDone;
    this.showGearAdjustmentDone = showGearAdjustmentDone;
    this.invalidateScore      = invalidateScore;
    this.isValidLatencyScore  = isValidLatencyScore;
    this.isValidIOScore       = isValidIOScore;
    this.isGoodFtue           = isGoodFtue;
    this.getLatencyScore      = getLatencyScore;
    this.getIOScore           = getIOScore;
    this.getLastSavedTime     = getLastSavedTime;
    this.onInvalidAudioDevice = onInvalidAudioDevice;

    return this;
  }


})(window, jQuery);