(function (context, $) {

  "use strict";

  context.JK = context.JK || {};
  context.JK.StepSelectGear = function (app, dialog) {

    var ALERT_NAMES = context.JK.ALERT_NAMES;
    var EVENTS  = context.JK.EVENTS;
    var ASSIGNMENT = context.JK.ASSIGNMENT;
    var VOICE_CHAT = context.JK.VOICE_CHAT;
    var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR;
    var gearUtils = context.JK.GearUtils;
    var modUtils = context.JK.ModUtils;

    var self = null;
    var $step = null;
    var logger = context.JK.logger;
    var rest = context.JK.Rest();
    var frameBuffers = new context.JK.FrameBuffers(app);
    var sampleRate = new context.JK.SampleRate(app);
    var gearTest = new context.JK.GearTest(app);
    var loopbackShowing = false;
    var adjustGearSettingsShowing = false;
    var wizard = null;

    // the goal of lastFailureAnalytics and trackedPass are to send only a single AudioTest 'Pass' or 'Failed' event, per FTUE wizard open/close
    var lastFailureAnalytics = {};
    var trackedPass = false;
    var firstTime = true;
    var helpTimeout = null;
    var passedOnce = false;

    var $watchVideoBtn = null;
    var $audioInput = null;
    var $audioOutput = null;
    var $inputChannels = null;
    var $outputChannels = null;
    var $knobs = null;
    var $sampleRate = null;
    var $selectSampleRate = null;
    var $adjustSettingsLink = null;
    var $adjustGearForIoFail = null;
    var $scoreReport = null;
    var $asioInputControlBtn = null;
    var $asioOutputControlBtn = null;
    var $resyncBtn = null;
    var $templateAudioPort = null;
    var $launchLoopbackBtn = null;
    var $instructions = null;
    var $templateDeviceNotValid = null;
    var $resyncStatus = null;
    var $resyncStatusText = null;
    var $latencyScoreBox = null;
    var $highLatencyNotice = null;


    var operatingSystem = null;
    var iCheckIgnore = false;
    var validDevice = false; // do we currently have a device selected that we can score against?

    // cached values between
    var deviceInformation = null;
    var lastSelectedDeviceInfo = null;
    var shownOutputProdOnce = false;
    var shownInputProdOnce = false;

    var selectedDeviceInfo = null;
    var musicPorts = null;
    var savedProfile = false;
    var queueUpdateDeviceList = false;
    var cancelRescanFunc = null;
    var initialScan = false;

    // returns a deviceInfo hash for the device matching the deviceId, or undefined.
    function findDevice(deviceId) {
      return deviceInformation[deviceId];
    }

    function selectedAudioInput() {
      return $audioInput.val();
    }

    function selectedAudioOutput() {
      return $audioOutput.val();
    }

    function setInputAudioDevice(value) {
      context.JK.dropdown($audioInput.val(value).easyDropDown('select', value.toString(), true))
    }

    function setOutputAudioDevice(value) {
      if(value != "" && value == selectedAudioInput()) {
        value = ''; // to force Same as Input
      }
      context.JK.dropdown($audioOutput.val(value).easyDropDown('select', value.toString(), true))
    }

    function initializeNextButtonState() {
      dialog.setNextState(gearTest.isGoodFtue() || dialog.getLoopbackWizard().getGearTest().isGoodFtue() || dialog.getAdjustGearSettings().getGearTest().isGoodFtue());
    }

    function initializeBackButtonState() {
      dialog.setBackState(!gearTest.isScoring());
    }

    function allInputDevices() {
      var allInputDevices = [];
      context._.each(deviceInformation, function (deviceInfo, deviceId) {

        if(deviceInfo.inputCount > 0) {
          allInputDevices.push({deviceId: deviceId, deviceInfo: deviceInfo});
        }
      });
      return allInputDevices;
    }

    function allOutputDevices() {
      var allOutputDevices = [];
      context._.each(deviceInformation, function (deviceInfo, deviceId) {

        if(deviceInfo.outputCount > 0) {
          allOutputDevices.push({deviceId: deviceId, deviceInfo: deviceInfo});
        }
      });
      return allOutputDevices;
    }

    function initializeAudioInput() {
      var optionsHtml = '';
      optionsHtml = '<option value="">Choose...</option>';
      context._.each(allInputDevices(), function (device) {
        optionsHtml += '<option title="' + device.deviceInfo.displayName + '" value="' + device.deviceId + '">' + device.deviceInfo.displayName + '</option>';
      });

      $audioInput.html(optionsHtml);
      context.JK.dropdown($audioInput);
      $audioInput.easyDropDown('enable')

      initializeAudioInputChanged();
    }

    function initializeAudioOutput() {
      var optionsHtml = '';
      optionsHtml = '<option value="">Same as Input</option>';
      context._.each(allOutputDevices(), function (device) {
        optionsHtml += '<option title="' + device.deviceInfo.displayName + '" value="' + device.deviceId + '">' + device.deviceInfo.displayName + '</option>';
      });
      $audioOutput.html(optionsHtml);
      context.JK.dropdown($audioOutput);
      $audioOutput.easyDropDown('disable'); // enable once they pick something in input

      initializeAudioOutputChanged();
    }

    // reloads the backend's channel state for the currently selected audio devices,
    // and update's the UI accordingly
    function initializeChannels() {
      musicPorts = context.jamClient.FTUEGetChannels();

      initializeInputPorts(musicPorts);
      initializeOutputPorts(musicPorts);
    }

    // select 2 (or 1) inputs and 2 outputs for the user.  required to get a latency score
    // also, arguably convenient
    function autoSelectMinimumValidChannels() {

      var audioInputDeviceId = selectedAudioInput();
      var audioOutputDeviceId = selectedAudioOutput();

      var $allInputs = $inputChannels.find('input[type="checkbox"]');

      if ($allInputs.length == 0) {
        // ERROR: not enough channels
        if(!audioInputDeviceId || audioInputDeviceId == '') {
          context.JK.prodBubble($audioInput.closest('.easydropdown-wrapper'), 'select-input', {}, {positions:['right', 'top']});
        }
        else {
          // this path should be impossible because we filter output devices with 0 inputs from the input device dropdown
          // but we might flip that, so it's nice to leave this in to catch us later
          context.JK.Banner.showAlert('To be a valid input audio device, the device must have at least 1 input port.');
        }
        return false;
      }

      // ensure 1, or preferably 2, input channels are selected
      var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked');
      var $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)');

      logger.debug("marking all unassigned inputs length=(" + $allInputs.length + ")")

      var maxTries = 12; // we only allow up to 6 tracks, 12 inputs

      for(var i = 0; i < maxTries; i++) {
        $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)');

        if($unassignedInputs.length == 0) {
          break;
        }
        $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked');
      }

      /**
      if ($assignedInputs.length == 0) {
        if ($allInputs.length >= 2) {
          logger.debug("selecting 2 inputs")
          $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked');
          // this is required because iCheck change handler re-writes the inputs.   So we have to refetch unassigned outputs
          $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)');
          $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked');
        }
        else {
          logger.debug("selecting 1 inputs")
          $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked');
        }
        logger.debug("selecting 1 inputs")
        $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked');
      }*/


      var $allOutputs = $outputChannels.find('input[type="checkbox"]');
      if ($allOutputs.length < 2) {
        if(!audioOutputDeviceId || audioOutputDeviceId == '') {
          context.JK.prodBubble($audioOutput.closest('.easydropdown-wrapper'), 'select-output', {}, {positions:['right', 'top'], duration:7000});
        }
        else {
          // this path indicates that the user has deliberately chosen a device, so we need to tell them that this device does not work with JamKazam
          context.JK.prodBubble($audioOutput.closest('.easydropdown-wrapper'), 'select-output', {}, {positions:['right', 'top'], duration:7000});
        }

        return false;
      }

      // ensure 2 outputs are selected
      var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked');
      var $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)');

      if ($assignedOutputs.length == 0) {
        logger.debug("selecting both outputs")
        $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked');
        // this is required because iCheck change handler re-writes the inputs.   So we have to refetch unassigned outputs
        $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)');
        $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked');
      }
      else if ($assignedOutputs.length == 1) {
        logger.debug("selecting 1 output to round out 2 total")
        $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked');
      }

      return true;
    }

    // during this phase of the FTUE, we have to assign selected input channels
    // to tracks. The user, however, does not have a way to indicate which channel
    // goes to which track (that's not until the next step of the wizard).
    // so, we just auto-generate a valid assignment
    function newInputAssignment() {
      var assigned = 0;
      context._.each(musicPorts.inputs, function (inputChannel) {
        if (gearUtils.isChannelAssigned(inputChannel)) {
          assigned += 1;
        }
      });

      var newAssignment = Math.floor(assigned / 2) + 1;
      return newAssignment;
    }

    function setUIToBackendState() {

    }
    function reassignInputChannels() {
      assertFTUEProfile();
      var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked');
      var assigned = 0;
      context._.each($assignedInputs, function (assignedInput) {
        var $assignedInput = $(assignedInput);
        var assignedChannelId = $assignedInput.attr('data-id');
        var newAssignment = Math.floor(assigned / 2) + 1;
        logger.debug("re-assigning input channel %o to track: %o", assignedChannelId, newAssignment);
        context.jamClient.TrackSetAssignment(assignedChannelId, true, newAssignment);
        assigned += 1;
      });
    }

    function reassignOutputChannels() {
      assertFTUEProfile();
      var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked');
      context._.each($assignedOutputs, function (assignedOutput) {
        var $assignedOutput = $(assignedOutput);
        var assignedChannelId = $assignedOutput.attr('data-id');
        logger.debug("re-assigning output channel %o", assignedChannelId);
        context.jamClient.TrackSetAssignment(assignedChannelId, true, ASSIGNMENT.OUTPUT);
      });
    }

    function inputChannelChanged() {
      if (iCheckIgnore) return;

      assertFTUEProfile();

      var $checkbox = $(this);
      var channelId = $checkbox.attr('data-id');
      var isChecked = $checkbox.is(':checked');

      if (isChecked) {
        var newAssignment = newInputAssignment();
        logger.debug("assigning input channel %o to track: %o", channelId, newAssignment);
        context.jamClient.TrackSetAssignment(channelId, true, newAssignment);
      }
      else {
        logger.debug("unassigning input channel %o", channelId);
        context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED);
        // unassigning creates a hole in our auto-assigned tracks.  reassign them all to keep it consistent
        reassignInputChannels();
      }

      initializeChannels();
    }

    // should be called in a ifChanged callback if you want to cancel.
    // you have to use this instead of 'return false' like a typical input 'change' event.
    function cancelICheckChange($checkbox) {
      iCheckIgnore = true;
      var checked = $checkbox.is(':checked');
      setTimeout(function () {
        if (checked) $checkbox.iCheck('uncheck').removeAttr('checked');
        else        $checkbox.iCheck('check').attr('checked', 'checked');
        iCheckIgnore = false;
      }, 1);
    }


    function outputChannelChanged() {
      if (iCheckIgnore) return;
      var $checkbox = $(this);
      var channelId = $checkbox.attr('data-id');
      var isChecked = $checkbox.is(':checked');

      // don't allow more than 2 output channels selected at once
      if ($outputChannels.find('input[type="checkbox"]:checked').length > 2) {
        context.JK.Banner.showAlert('You can only have a maximum of 2 output ports selected.');
        // can't allow uncheck of last output
        cancelICheckChange($checkbox);
        return;
      }

      if (isChecked) {
        logger.debug("assigning output channel %o", channelId);
        context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.OUTPUT);
      }
      else {
        logger.debug("unassigning output channel %o", channelId);
        context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED);
      }

      initializeChannels();
    }

    function initializeInputPorts(musicPorts) {
      $inputChannels.empty();
      var inputPorts = musicPorts.inputs;
      context._.each(inputPorts, function (inputChannel) {
        var $inputChannel = $(context._.template($templateAudioPort.html(), inputChannel, { variable: 'data' }));
        var $checkbox = $inputChannel.find('input');
        if (gearUtils.isChannelAssigned(inputChannel)) {
          logger.debug("channel is assigned", inputChannel)
          $checkbox.attr('checked', 'checked');
        }
        else {
          logger.debug("channel is not assigned", inputChannel)
        }
        context.JK.checkbox($checkbox);
        $checkbox.on('ifChanged', inputChannelChanged);
        $inputChannels.append($inputChannel);
      });
    }

    function initializeOutputPorts(musicPorts) {
      $outputChannels.empty();
      var outputChannels = musicPorts.outputs;
      context._.each(outputChannels, function (outputChannel) {
        var $outputPort = $(context._.template($templateAudioPort.html(), outputChannel, { variable: 'data' }));
        var $checkbox = $outputPort.find('input');
        if (gearUtils.isChannelAssigned(outputChannel)) {
          $checkbox.attr('checked', 'checked');
        }
        context.JK.checkbox($checkbox);
        $checkbox.on('ifChanged', outputChannelChanged);
        $outputChannels.append($outputPort);
      });
    }

    function initializeLoopback() {
      $launchLoopbackBtn.unbind('click').click(function() {

        $(dialog.getLoopbackWizard().getDialog()).one(EVENTS.DIALOG_CLOSED, function() {
          loopbackShowing = false;

          if(dialog.getLoopbackWizard().getGearTest().isGoodFtue()) {
            gearTest.resetScoreReport();
            gearTest.showLoopbackDone();
            setTimeout(function() {
              context.JK.prodBubble(dialog.getWizard().getNextButton(), 'can-move-on', {}, {positions:['top'], offsetParent: dialog.getWizard().getDialog()});
            }, 300);
          }

          initializeNextButtonState();
          initializeBackButtonState();
        })

        loopbackShowing = true;
        app.layout.showDialog('loopback-wizard')
        return false;
      })
    }

    function onAdjustGearRequested()
    {
      if(gearTest.isScoring()) {logger.debug("ignoring adjust-gear request while scoring"); return false;}

      app.layout.showDialog('adjust-gear-speed-dialog').one(EVENTS.DIALOG_CLOSED, function(e, data) {
        adjustGearSettingsShowing = false;

        var adjustGearTest = data.result;

        if(!data.canceled) {
          if(adjustGearTest.isGoodFtue()) {
            // update our own frame buffers to reflect any changes made by the adjust dialog
            frameBuffers.setFramesize(context.jamClient.FTUEGetFrameSize())
            frameBuffers.setBufferIn(context.jamClient.FTUEGetInputLatency())
            frameBuffers.setBufferOut(context.jamClient.FTUEGetOutputLatency())

            gearTest.resetScoreReport();
            gearTest.showGearAdjustmentDone();

            setTimeout(function() {
              context.JK.prodBubble(dialog.getWizard().getNextButton(), 'can-move-on', {}, {positions:['top'], offsetParent: dialog.getWizard().getDialog()});
            }, 300);
          }

          initializeNextButtonState();
          initializeBackButtonState();
        }
        else {
          logger.debug("adjust-gear-speed was cancelled; ignoring")
        }
      })

      adjustGearSettingsShowing = true;
      return false;
    }

    function initializeFormElements() {
      if (!deviceInformation) throw "devices are not initialized";

      initializeAudioInput();
      initializeAudioOutput();
      initializeLoopback();
    }

    function clearInputPorts() {
      $inputChannels.empty();
    }

    function clearOutputPorts() {
      $outputChannels.empty();
    }

    function audioInputDeviceUnselected() {
      validDevice = false;
      setOutputAudioDevice('');
      resetState();
    }

    function renderScoringStarted() {
      initializeBackButtonState();
      initializeNextButtonState();
      freezeAudioInteraction();
    }

    function renderScoringStopped() {
      initializeNextButtonState();
      initializeBackButtonState();
      unfreezeAudioInteraction();
    }

    function freezeAudioInteraction() {
      logger.debug("freezing audio interaction");
      $audioInput.attr("disabled", "disabled").easyDropDown('disable');
      $audioOutput.attr("disabled", "disabled").easyDropDown('disable');
      frameBuffers.disable();
      sampleRate.disable();
      $asioInputControlBtn.on("click", false).addClass('disabled');
      $asioOutputControlBtn.on("click", false).addClass('disabled');
      $resyncBtn.on('click', false).addClass('disabled');
      iCheckIgnore = true;
      $inputChannels.find('input[type="checkbox"]').iCheck('disable');
      $outputChannels.find('input[type="checkbox"]').iCheck('disable');
    }

    function unfreezeAudioInteraction() {
      logger.debug("unfreezing audio interaction");
      $audioInput.removeAttr("disabled").easyDropDown('enable');
      $audioOutput.removeAttr("disabled").easyDropDown('enable');
      frameBuffers.enable();
      sampleRate.enable();
      $asioInputControlBtn.off("click", false).removeClass('disabled');
      $asioOutputControlBtn.off("click", false).removeClass('disabled')
      $resyncBtn.off('click', false).removeClass('disabled')
      $inputChannels.find('input[type="checkbox"]').iCheck('enable');
      $outputChannels.find('input[type="checkbox"]').iCheck('enable');
      iCheckIgnore = false;
    }



    function invalidateScore() {
      gearTest.invalidateScore();
      dialog.getLoopbackWizard().getGearTest().invalidateScore();
      dialog.getAdjustGearSettings().getGearTest().invalidateScore();
      initializeNextButtonState();
    }

    function initializeASIOButtons() {
      $asioInputControlBtn.unbind('click').click(function () {
        if(gearTest.isScoring()) {
          return false;
        }
        context.jamClient.StopAudio();
        setTimeout(function() {
          context.jamClient.FTUEOpenControlPanel(selectedAudioInput());
        }, 2000)

        return false;
      });
      $asioOutputControlBtn.unbind('click').click(function () {
        if(gearTest.isScoring()) {
          return false;
        }

        context.jamClient.FTUEOpenControlPanel(selectedAudioOutput());
        return false;
      });
    }

    function onSampleRateChanged() {
      console.log("onSampleRateChanged: ", sampleRate.selectedSampleRate())
      //context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
      jamClient.FTUESetPreferredMixerSampleRate(sampleRate.selectedSampleRate());
      jamClient.FTUESetPreferredOutputSampleRate(sampleRate.selectedSampleRate());
      invalidateScore();
      safeAtttemptScore();
    }

    function onFramesizeChanged() {
      //context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
      updateDefaultBuffers();
      jamClient.FTUESetFrameSize(frameBuffers.selectedFramesize());
      invalidateScore();
      safeAtttemptScore();
    }
    
    function onBufferInChanged() {
      //context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
      jamClient.FTUESetInputLatency(frameBuffers.selectedBufferIn());
      invalidateScore();
      safeAtttemptScore()
    }
    
    function onBufferOutChanged() {
      //context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
      jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut());
      invalidateScore();
      safeAtttemptScore()
    }

    function safeAtttemptScore() {
      scheduleRescanSystem(function() {
        if (getSelectedInputs().length > 0 && getSelectedOutputs().length == 2) {
          logger.debug("after rescan, ready to attempt score")
          attemptScore();
        }
        else {
          logger.debug("after rescan, not ready to attempt score")
        }
      }, 500, false);
      return false;
    }

    function getSelectedInputs() {
      return $inputChannels.find('input[type="checkbox"]:checked');
    }

    function getSelectedOutputs() {
      return $outputChannels.find('input[type="checkbox"]:checked');
    }

    function initializeResync() {
      $resyncBtn.unbind('click').click(function () {

        if ($highLatencyNotice) {
          $highLatencyNotice.btOff()
          $highLatencyNotice = null;
        }

        safeAtttemptScore()
      })
    }

    function rescan() {

      // start audio if already running, false = don't reload last audio configuration, true = re-init tracks
      var result = context.jamClient.ReloadAudioSystem(context.jamClient.IsAudioStarted(), false, true);

      // get the current list of input devices, and output devices, in the UI

      // these are arrays with this structure
      // {deviceId: id, deviceInfo: deviceInfo}
      var originalInputDevices = allInputDevices();
      var originalOutputDevices = allOutputDevices();
      var originalInputDeviceIds = originalInputDevices.map(function(device){return device.deviceId});
      var originalOutputDevicesId = originalOutputDevices.map(function(device){return device.deviceId});
      var selectedInput = selectedAudioInput();
      var selectedOutput = selectedAudioOutput();

      // reload device info now that the scan is complete
      deviceInformation = gearUtils.loadDeviceInfo();

      var inputDevices = allInputDevices();
      var outputDevices = allOutputDevices();
      var inputDeviceIds = inputDevices.map(function(device){return device.deviceId});
      var outputDeviceIds = outputDevices.map(function(device){return device.deviceId});

      var missingInputDeviceIds = $(originalInputDeviceIds).not(inputDeviceIds).get();
      var newInputDeviceIds = $(inputDeviceIds).not(originalInputDeviceIds).get();

      var missingOutputDeviceIds = $(originalOutputDevicesId).not(outputDeviceIds).get();
      var newOutputDeviceIds = $(outputDeviceIds).not(originalOutputDevicesId).get();

      // these are used later to show prod bubbles
      var missingInputDevice = null;
      var newInputDevice = null;
      var missingOutputDevice = null;
      var newOutputDevice = null;

      if(missingInputDeviceIds.length > 0 || newOutputDeviceIds.length > 0) {

        initializeAudioInput();

        if(missingInputDeviceIds.indexOf(selectedInput) > -1) {
          // the user had a device selected that's no longer here. we need to notify them of this, and auto-select 'no device'
          audioInputDeviceUnselected();

          missingInputDevice = originalInputDevices.filter(function(inputDevice) { return selectedInput == inputDevice.deviceId })[0].deviceInfo.displayName;
        }
        else {
          setInputAudioDevice(selectedInput);

          if(missingInputDeviceIds.length > 0) {
            missingInputDevice = originalInputDevices.filter(function(inputDevice) { return missingInputDeviceIds[0] == inputDevice.deviceId })[0].deviceInfo.displayName;
          }
          if(newInputDeviceIds.length > 0) {
            newInputDevice = inputDevices.filter(function(inputDevice) { return newInputDeviceIds[0] == inputDevice.deviceId })[0].deviceInfo.displayName;
          }
        }
      }

      if(missingOutputDeviceIds.length > 0 || newOutputDeviceIds.length > 0) {

        initializeAudioOutput();

        if(missingOutputDeviceIds.indexOf(selectedOutput) > -1) {
          // the user had a device selected that's no longer here. we need to notify them of this, and auto-select 'no device'
          setOutputAudioDevice('');

          missingInputDevice = originalOutputDevices.filter(function(outputDevice) { return selectedOutput == outputDevice.deviceId })[0].deviceInfo.displayName;
        }
        else {
          setOutputAudioDevice(selectedOutput);
          if(missingOutputDeviceIds.length > 0) {
            missingOutputDevice = originalOutputDevices.filter(function(outputDevice) { return missingOutputDeviceIds[0] == outputDevice.deviceId })[0].deviceInfo.displayName;
          }
          if(newOutputDeviceIds.length > 0) {
            newOutputDevice = outputDevices.filter(function(outputDevice) { return newOutputDeviceIds[0] == outputDevice.deviceId })[0].deviceInfo.displayName;
          }
        }
      }

      // handle prod bubbles in case devices are gone
      if(missingInputDevice || missingOutputDevice || newInputDevice || newOutputDevice) {
        if(missingInputDevice == missingOutputDevice || newInputDevice == newOutputDevice) {
          // we only notify over the input device in the case the device is new/missing from both input/output, to reduce too much bubble action
          context.JK.prodBubble($audioInput.closest('.easydropdown-wrapper'), 'gear-wizard-inputs-changed', {missingInputDevice: missingInputDevice, newInputDevice: newInputDevice}, {positions:['top'], duration:8000});
        }
        else {
          if(missingInputDevice || newInputDevice) {
            context.JK.prodBubble($audioInput.closest('.easydropdown-wrapper'), 'gear-wizard-inputs-changed', {missingInputDevice: missingInputDevice, newInputDevice: newInputDevice}, {positions:['top'], duration:8000});
          }
          if(missingOutputDevice || newOutputDevice) {
            context.JK.prodBubble($audioOutput.closest('.easydropdown-wrapper'), 'gear-wizard-outputs-changed', {missingOutputDevice: missingOutputDevice, newOutputDevice: newOutputDevice}, {positions:[ 'top'], duration:8000});
          }
        }
      }

      // repair channel selections, because the backend cleared them in RebuildAudioSystem
      reassignInputChannels();
      reassignOutputChannels();
    }

    function cancelRescan() {
      if(cancelRescanFunc) {
        cancelRescanFunc();
        cancelRescanFunc = null;
      }
    }

    function scheduleRescanSystem(after, time, checkingForGear) {

      cancelRescanFunc = gearUtils.scheduleAudioRestart('gear-wizard-select-gear', time,
      function() {
        freezeAudioInteraction();
        $resyncBtn.addClass('scanning').text('RESYNC')

        $resyncStatusText.text(checkingForGear ? 'CHECKING GEAR' : 'RESYNCING...');
        $resyncStatus.show();
      },
      function(cancel) {
        $resyncBtn.removeClass('scanning').text('RESYNC')
        unfreezeAudioInteraction();
        $resyncStatus.hide();

        if(!cancel) {
          rescan();

          if(after) {
            after();
          }
        }
      });
    }

    // sets selectedDeviceInfo, which contains id, behavior, and info for input and output device
    function cacheCurrentAudioInfo() {

      var audioInputDeviceId = selectedAudioInput();
      var audioOutputDeviceId = selectedAudioOutput();
      if (!audioInputDeviceId) {
        audioInputDeviceUnselected();
        return false;
      }

      var audioInputDevice = findDevice(audioInputDeviceId);
      if (!audioInputDevice) {
        context.JK.alertSupportedNeeded('Unable to find information for input device: ' + audioInputDeviceId);
        return false;
      }

      if (!audioOutputDeviceId) {
        audioOutputDeviceId = audioInputDeviceId;
      }
      var audioOutputDevice = findDevice(audioOutputDeviceId);
      if (!audioInputDevice) {
        context.JK.alertSupportedNeeded('Unable to find information for output device: ' + audioOutputDeviceId);
        return false;
      }

      if(savedProfile && jamClient.FTUEGetInputMusicDevice() != audioInputDeviceId || jamClient.FTUEGetOutputMusicDevice() != audioOutputDeviceId) {
        // if they want to change an input or output device, but we have a saved (named) profile
        // then we need to delete the current profile and create a new one
        // because there is no way to rename a profile, and the profile name has the device's name in it
        var profileName = context.jamClient.FTUEGetMusicProfileName();
        logger.debug("invaliding previously saved profile: " + profileName);

        dialog.createFTUEProfile();
        // restore user selections because newSession is called by createFTUEProfile(), invalidating dropdowns
        setInputAudioDevice(audioInputDeviceId);
        setOutputAudioDevice(audioOutputDeviceId);

        context.jamClient.TrackDeleteProfile(profileName);

      }

      shownOutputProdOnce = false;
      shownInputProdOnce = false;

      lastSelectedDeviceInfo = selectedDeviceInfo;

      selectedDeviceInfo = gearUtils.selectedDeviceInfo(audioInputDeviceId, audioOutputDeviceId, deviceInformation)

      return true;
    }

    function changeDevice() {

      var audioInputDeviceId = selectedDeviceInfo.input.id;
      var audioOutputDeviceId = selectedDeviceInfo.output.id;

      if(audioInputDeviceId) {
        $audioOutput.easyDropDown('enable');
      }

      // don't re-assign input/output audio devices because it disturbs input/output track association
      if (jamClient.FTUEGetInputMusicDevice() != audioInputDeviceId) {
        jamClient.FTUESetInputMusicDevice(audioInputDeviceId);
      }
      if (jamClient.FTUEGetOutputMusicDevice() != audioOutputDeviceId) {
        jamClient.FTUESetOutputMusicDevice(audioOutputDeviceId);
      }

      initializeChannels();

      validDevice = autoSelectMinimumValidChannels();

      if (!validDevice) {
        $adjustSettingsLink.hide();
        return false;
      }

      jamClient.FTUESetInputLatency(frameBuffers.selectedBufferIn());
      jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut());
      jamClient.FTUESetFrameSize(frameBuffers.selectedFramesize());

      // further, check if we have both inputs and outputs defined; if so, show ? to allow launch of adjust gear settings dialog
      if(modUtils.getGear('show_frame_options')) {
        $adjustSettingsLink.show();
      }

      return true;
    }

    function audioDeviceChanged() {
      if(!cacheCurrentAudioInfo()) {
        return;
      }
      updateDialogForCurrentDevices();
      if (changeDevice()) {
        attemptScore();
      }
    }

    function isInputAudioTypeDifferentFromLastTime() {
      return lastSelectedDeviceInfo && (lastSelectedDeviceInfo.input.info.type != selectedDeviceInfo.input.info.type);
    }

    function isOutputAudioTypeDifferentFromLastTime() {
      return lastSelectedDeviceInfo && isInputOutputDifferentTypes() && (lastSelectedDeviceInfo.output.info.type != selectedDeviceInfo.output.info.type)
    }

    function isInputOutputDifferentTypes() {
      return selectedDeviceInfo && (selectedDeviceInfo.input.info.type != selectedDeviceInfo.output.info.type);
    }

    function updateDialogForCurrentDevices() {
      if(selectedDeviceInfo) {
        var inputBehavior = selectedDeviceInfo.input.behavior;
        var outputBehavior = selectedDeviceInfo.output.behavior;
      }
      else {
        var inputBehavior = null;
        var outputBehavior = null;
      }

      // deal with sample rate
      if(selectedDeviceInfo  && context.jamClient.FTUESetPreferredMixerSampleRate) {
        // get the preferred sample rate for the device, and set that as the initial value of the sample rate dropdown
        // except for WKS; we just default to 48
        if (selectedDeviceInfo.input.info.type == 'Win32_wdm') {
          var inputSampleRate = 48000;
        }
        else if (selectedDeviceInfo.input.info.port_audio_name != 'Default Input') {
          var inputSampleRate = context.jamClient.FTUEgetInputDeviceSampleRate(selectedDeviceInfo.input.info.port_audio_name);
        }
        else {
          var inputSampleRate = 44100;
        }

        if (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm') {
          console.log("WDM selected; expand WDM framesize")
          frameBuffers.switchToWdm()
        }
        else {
          console.log("NOT WDM selected; go to normal framesizes")
          frameBuffers.switchOffWdm()
        }
        // ignore all this; just set it to
        inputSampleRate = 'DEVICE_DEFAULT'
        logger.debug("applying the sample rate based on input device: " + selectedDeviceInfo.input.id + " (" + inputSampleRate + ")");
        sampleRate.selectSampleRate(inputSampleRate);
        context.jamClient.FTUESetPreferredMixerSampleRate(sampleRate.selectedSampleRate());
        context.jamClient.FTUESetPreferredOutputSampleRate(sampleRate.selectedSampleRate());

        $sampleRate.show();
      }
      else {
        $sampleRate.hide();
      }

      // deal with watch video
      if(isInputOutputDifferentTypes()) {
      }
      else {
      }

      // handle framesize/buffers
      if (inputBehavior && (inputBehavior.showKnobs || outputBehavior.showKnobs || modUtils.getGear('show_frame_options'))) {
        $knobs.show();
      }
      else {
        $knobs.hide();
        $adjustSettingsLink.hide();
      }

      // handle ASIO visibility
      if (inputBehavior) {
        if (inputBehavior.showASIO) {
          $asioInputControlBtn.show();
        }
        else {
          $asioInputControlBtn.hide();
        }
        if(outputBehavior.showASIO && (selectedDeviceInfo.input.id != selectedDeviceInfo.output.id)) {
          $asioOutputControlBtn.show();
        }
        else {
          $asioOutputControlBtn.hide();
        }
      }
      else {
        // show no ASIO buttons
        $asioInputControlBtn.hide();
        $asioOutputControlBtn.hide();
      }

      updateDefaultFrameSize();

      updateDefaultBuffers();
    }

    function updateDefaultFrameSize() {
      if(selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' ||  selectedDeviceInfo.output.info.type == 'Win32_wdm')) {
        frameBuffers.setFramesize('10');
      }
      else if(selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_asio' ||  selectedDeviceInfo.output.info.type == 'Win32_asio')) {
        frameBuffers.setFramesize('2')
      }
      else {
        frameBuffers.setFramesize('2.5')
      }

      jamClient.FTUESetFrameSize(frameBuffers.selectedFramesize());
    }


    function updateDefaultBuffers() {
      gearUtils.updateDefaultBuffers(selectedDeviceInfo, frameBuffers)
    }

    // 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(refocused) {
      gearTest.attemptScore(selectedDeviceInfo, refocused);
    }

    function initializeAudioInputChanged() {
      $audioInput.unbind('change').change(audioDeviceChanged);
    }

    function initializeAudioOutputChanged() {
      $audioOutput.unbind('change').change(audioDeviceChanged);
    }

    function onGearTestStarted(e, data) {
      renderScoringStarted();
    }

    function onGearTestDone(e, data) {
      renderScoringStopped();
      gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true);
      if(queueUpdateDeviceList) {
        queueUpdateDeviceList = false;
        updateDeviceList();
      }

      if(!data.validLatencyScore) {
        if (selectedDeviceInfo.input.info.type.indexOf('Win32_asio') > -1) {
          prodUserAboutHighLatency($latencyScoreBox, 'asio')
        }
        else if (selectedDeviceInfo.output.info.type.indexOf('Win32_asio') > -1) {
          prodUserAboutHighLatency($latencyScoreBox, 'asio')
        }
        else if (selectedDeviceInfo.input.info.type == 'MacOSX_builtin' || selectedDeviceInfo.output.info.type == 'MacOSX_builtin') {
          prodUserAboutHighLatency($latencyScoreBox, 'macosx-builtin')
        }
        else {
          prodUserAboutHighLatency($latencyScoreBox, 'generic')
        }
      }
    }

    function getLastAudioTestFailAnalytics() {
      return lastFailureAnalytics;
    }

    function storeLastFailureForAnalytics(platform, reason, data) {
      if(!trackedPass) {
        lastFailureAnalytics = {
          platform: platform,
          reason: reason,
          data: data
        }
      }
    }

    function prodUserAboutHighLatency($btn, additional) {

      setTimeout(function() {
        $highLatencyNotice = context.JK.prodBubble($btn, 'high-latency-notice', {additional: additional, os:operatingSystem}, {duration: 20000, width:'400px', positions:['top']});
      }, 300)
    }

    function prodUserToTweakASIOSettings($btn) {
      setTimeout(function() {
        context.JK.prodBubble($btn, 'tweak-asio-settings', {}, {positions:['top']});
      }, 300)
    }

    function onGearTestFail(e, data) {
      renderScoringStopped();
      gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true);

      if(data.reason == "io") {


        //storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.latency, data.latencyScore);

        if(data.ioTarget == 'bad') {
          storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.ioTarget, data.ioTargetScore);
        }
        else {
          storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.ioVariance, data.ioVarianceScore);
        }
      }
      else if(data.reason == 'invalid_configuration') {
        if(data.data.reason == 'device_failure') {

          var specificErrors = [];
          if(selectedDeviceInfo.input.id == selectedDeviceInfo.output.id && data.data.input) {
            specificErrors.push("The configured device (" + selectedDeviceInfo.input.info.displayName + ") is not connected or not functioning.");
          }
          else {
            if(data.data.input) {
              specificErrors.push("The configured input device (" + selectedDeviceInfo.input.info.displayName + ") is not connected or not functioning.");
            }
            if(data.data.output) {
              specificErrors.push("The configured output device (" + selectedDeviceInfo.output.info.displayName + ") is not connected or not functioning.");
            }
          }

          var specificResolutions = [];

          if(selectedDeviceInfo.input.info.type.indexOf('Win32_asio') > -1 || selectedDeviceInfo.output.info.type.indexOf('Win32_asio') > -1) {
           // specificResolutions.push("Read over this <a rel='external' href='https://jamkazam.desk.com/customer/portal/articles/1723489-jamkazam-thinks-your-gear-is-disconnected---windows-only'>article</a>")
            specificResolutions.push("Select the ASIO SETTINGS... button and try different settings.");
          }

          if(selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm') {
           // specificResolutions.push("Read over this <a rel='external' href='https://jamkazam.desk.com/customer/portal/articles/1723489-jamkazam-thinks-your-gear-is-disconnected---windows-only'>article</a>")
            specificResolutions.push("Change the Frame, Buffer In, or Buffer Out settings.");
          }

          specificResolutions.push("Reconnect the device if possible.");
          specificResolutions.push("Restart the JamKazam application.");
          specificResolutions.push("Make sure you have the latest drivers installed for your gear.");
          specificResolutions.push("In rare cases, restarting your computer can help.");

          context.JK.Banner.showAlert('Invalid Audio Device',
            $(context._.template($templateDeviceNotValid.html(), {specific_errors: specificErrors, specific_resolutions: specificResolutions }, { variable: 'data' })));
        }
        else {
          context.JK.alertSupportedNeeded("Unable to configure device.  " + data.data)
        }
      }
      else {
        logger.error("unknown reason in onGearTestFail: " + data.reason)
      }

      if(queueUpdateDeviceList) {
        queueUpdateDeviceList = false;
        updateDeviceList();
      }
    }

    function onGearTestInvalidated(e, data) {
      initializeNextButtonState();
    }


    function handleHelp() {
      if (operatingSystem == "Win32") {
        return "https://jamkazam.freshdesk.com/support/solutions/articles/66000123617-setup-wizard-windows-step-2-setting-up-your-audio-interface"
      }
      else {
        return "https://jamkazam.freshdesk.com/support/solutions/articles/66000123592-setup-wizard-mac-step-2-setting-up-your-audio-interface"
      }
    }

    function handleNext() {

      var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked');
      var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked');

      var errors = [];
      if($assignedInputs.length == 0) {
        errors.push("There must be at least one selected input ports.");
      }
      if($assignedInputs.length > 12) {
        errors.push("There can only be up to 12 selected inputs ports.");
      }
      if($assignedOutputs.length != 2) {
        errors.push("There must be exactly 2 selected output ports.");
      }
      var $errors = $('<ul></ul>');
      context._.each(errors, function(error) {
        $errors.append('<li>' + error + '</li>');
      });

      if(errors.length > 0) {
        context.JK.Banner.showAlert({html:$errors});
        return false;
      }

      if (!passedOnce) {
        passedOnce = true;
        autoAssignToSingleInput();
      }

      if(!savedProfile) {
        context.jamClient.FTUESetMusicProfileName(gearUtils.createProfileName(selectedDeviceInfo));
        var result = context.jamClient.FTUESave(true);
        if(result && result.error) {
          // failed
          logger.warn("unable to FTUESave(true). reason:" + result.detail);
          context.JK.alertSupportedNeeded("Unable to persist the audio profile. " + result.detail);
          return false;
        }
        else {
          context.JK.GA.trackAudioTestCompletion(context.JK.detectOS());
          trackedPass = true;
          lastFailureAnalytics = null;
          savedProfile = true;
        }
      }

      // keep the shared state between step 2 and step 3 up-to-date
      wizard.setChosenInputs(context._.map($assignedInputs, function(input) { return $(input).attr('data-id') }));

      if (helpTimeout) {
        clearTimeout(helpTimeout);
      }

      $sampleRate.btOff()


      return true;
    }

    function autoAssignToSingleInput() {
      var $allInputs = $inputChannels.find('input[type="checkbox"]')
      if ($allInputs.length == 0) {
        logger.debug("no inputs; bailing autoAssignToSingleInput")
        return;
      }
      var $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)')
      $allInputs.eq(0).iCheck('check').attr('checked', 'checked')

      var maxTries = 36;
      for(var i = 0; i < maxTries; i++) {
        var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked')
        var $allInputs = $inputChannels.find('input[type="checkbox"]')
        var firstInputDomNode = $allInputs.get(0)
        if ($assignedInputs.length == 1) {
          break;
        }
        else {
          $assignedInputs.each(function(index, item) {
            var $node = $(item)
            if ($node.get(0) == firstInputDomNode) {
              logger.debug("skipping firstInputDomNode")
            }
            else {
              // the goal of this loop is to just uncheck one.  once it's done that, it breaks the loop, so that assigned can be fetched again
              $node.iCheck('uncheck').removeAttr('checked')
              return false;
            }
          })
        }
      }
    }

    function onFocus() {
      if(validDevice && !loopbackShowing && !adjustGearSettingsShowing && !gearTest.isScoring() && getSelectedInputs().length >  0 && getSelectedOutputs().length == 2 ) {
        //scheduleRescanSystem(function() { attemptScore(true); }, 3000, false)
        context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
      }
    }

    function newSession() {
      passedOnce = false;
      savedProfile = false;
      initialScan = false;
      deviceInformation = gearUtils.loadDeviceInfo();
      sampleRate.setLastSelectedDevice(null);
      resetState();
      initializeFormElements();
      initializeNextButtonState();
      initializeASIOButtons();
      initializeResync();
    }

    function beforeWizardShow() {
      lastFailureAnalytics = null;
      trackedPass = false;
    }

    function beforeShow() {
      $(window).on('focus', onFocus);
      context.JK.onBackendEvent(ALERT_NAMES.USB_CONNECTED, 'select-gear', onUsbDeviceConnected);
      context.JK.onBackendEvent(ALERT_NAMES.USB_DISCONNECTED, 'select-gear', onUsbDeviceDisconnected);
      context.JK.onBackendEvent(ALERT_NAMES.SAMPLERATE_CONFIGURATION_BAD, 'select-gear', onSampleRateBad)

      initializeNextButtonState();
      context.JK.onBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'gear_test', gearTest.onInvalidAudioDevice);

      if(!initialScan) {
        scheduleRescanSystem(null, 3000, true);
        initialScan = true;
      }
      else {
        initializeChannels();
      }
      prodInstructions();


    }

    function prodInstructions() {
      if (firstTime && !helpTimeout) {

        helpTimeout = setTimeout(function() {
          helpTimeout = null
          firstTime = false;

          context.JK.HelpBubbleHelper.ftueStep2Instructions($watchVideoBtn)
        }, 2000)
      }
    }

    function beforeHide() {
      cancelRescan();
      logger.debug("unregistering focus watch")
      context.JK.offBackendEvent(ALERT_NAMES.USB_CONNECTED, 'select-gear', onUsbDeviceConnected);
      context.JK.offBackendEvent(ALERT_NAMES.USB_DISCONNECTED, 'select-gear', onUsbDeviceDisconnected);
      context.JK.offBackendEvent(ALERT_NAMES.SAMPLERATE_CONFIGURATION_BAD, 'select-gear', onSampleRateBad)
      $(window).off('focus', onFocus);
      context.JK.offBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'gear_test', gearTest.onInvalidAudioDevice);
    }

    function onUsbDeviceConnected() {
      if(!context.jamClient.IsFrontendVisible()) {return;} // don't handle USB events when minimized

      updateDeviceList();
    }

    function onUsbDeviceDisconnected() {
      if(!context.jamClient.IsFrontendVisible()) {return;} // don't handle USB events when minimized

      updateDeviceList();
    }

    function onSampleRateBad() {
      context.JK.HelpBubbleHelper.ftueStep2SampleRate($sampleRate, $sampleRate.closest('.dialog'))
    }

    function updateDeviceList() {
      if(gearTest.isScoring()) {
        queueUpdateDeviceList = true;
      }
      else {
        scheduleRescanSystem(null, 5000, false);
      }
    }
    function resetState() {
      selectedDeviceInfo = null;
      invalidateScore();
      clearInputPorts();
      clearOutputPorts();
      frameBuffers.resetValues();
      updateDialogForCurrentDevices();
    }

    function assertFTUEProfile() {
      if(savedProfile) {return;} // once we save the profile, it's name no longer starts with FTUE
      var profileName = context.jamClient.FTUEGetMusicProfileName();

      if(profileName && profileName.indexOf('FTUE') != 0) {
        logger.debug("the profile name *must* start with FTUE during step 2. name=" + profileName)
          context.JK.alertSupportedNeeded('The application is no longer modifying a new profile.  Please restart the application and try the gear wizard again.');
      }
    }

    function initialize(_$step, _wizard, _operatingSystem) {
      $step = _$step;
      wizard = _wizard;

      $watchVideoBtn = $step.find('.watch-video');
      $audioInput = $step.find('.select-audio-input-device');
      $audioOutput = $step.find('.select-audio-output-device');
      $templateDeviceNotValid = $('#device-not-valid');

      $inputChannels = $step.find('.input-ports');
      $outputChannels = $step.find('.output-ports');
      $knobs = $step.find('.frame-and-buffers');
      $sampleRate = $step.find('.sample-rate')
      $selectSampleRate = $step.find('.select-sample-rate');
      $adjustSettingsLink = $knobs.find('.adjust-gear-settings')
      $adjustGearForIoFail = $step.find(".adjust-gear-for-io-fail")
      $scoreReport = $step.find('.results');
      $asioInputControlBtn = $step.find('.asio-settings-input-btn');
      $asioOutputControlBtn = $step.find('.asio-settings-output-btn');
      $resyncBtn = $step.find('.resync-btn');
      $templateAudioPort = $('#template-audio-port');
      $launchLoopbackBtn = $step.find('.loopback-test');
      $instructions = $step.find('.instructions');
      $resyncStatus = $step.find('.resync-status');
      $resyncStatusText = $step.find('.resynctext');
      $latencyScoreBox = $step.find('.latency-score-section')

      operatingSystem = _operatingSystem;

      $watchVideoBtn.attr('href', handleHelp())

      frameBuffers.initialize($knobs);
      $(frameBuffers)
        .on(frameBuffers.FRAMESIZE_CHANGED, onFramesizeChanged)
        .on(frameBuffers.BUFFER_IN_CHANGED, onBufferInChanged)
        .on(frameBuffers.BUFFER_OUT_CHANGED, onBufferOutChanged)
        .on(frameBuffers.ADJUST_GEAR_LINK_CLICKED, onAdjustGearRequested)

      gearTest.initialize($scoreReport, true)
      $(gearTest)
        .on(gearTest.GEAR_TEST_START, onGearTestStarted)
        .on(gearTest.GEAR_TEST_DONE, onGearTestDone)
        .on(gearTest.GEAR_TEST_FAIL, onGearTestFail)
        .on(gearTest.GEAR_TEST_INVALIDATED_ASYNC, onGearTestInvalidated)
      $adjustGearForIoFail.click(onAdjustGearRequested);

      sampleRate.initialize($selectSampleRate)
      $(sampleRate).on(sampleRate.SAMPLERATE_CHANGED, onSampleRateChanged);
    }

    this.getLastAudioTestFailAnalytics = getLastAudioTestFailAnalytics;
    this.handleNext = handleNext;
    this.newSession = newSession;
    this.handleHelp = handleHelp;
    this.beforeShow = beforeShow;
    this.beforeHide = beforeHide;
    this.beforeWizardShow = beforeWizardShow;
    this.initialize = initialize;

    self = this;
    return this;
  };

})(window, jQuery);