/**
 * FtueAudioSelectionScreen
 * Javascript that goes with the screen where initial
 * selection of the audio devices takes place.
 * Corresponds to /#ftue2
 */
(function (context, $) {

  "use strict";

  context.JK = context.JK || {};
  context.JK.FtueWizard = function (app) {
    context.JK.FtueWizard.latencyTimeout = true;
    context.JK.FtueWizard.latencyMS = Number.MAX_VALUE;
    var rest = context.JK.Rest();

    var logger = context.JK.logger;
    var jamClient = context.jamClient;
    var win32 = true;
    var batchModify = false;
    var pendingFtueSave = false;
    var successfulFtue = false;

    // tracks in the loopback FTUe what the currently chosen audio driver is
    var currentAudioDriverId = null;

    var deviceSetMap = {
      'audio-input': jamClient.FTUESetMusicInput,
      'audio-output': jamClient.FTUESetMusicOutput,
      'voice-chat-input': jamClient.FTUESetChatInput
    };

    var faderMap = {
      'ftue-2-audio-input-fader': jamClient.FTUESetInputVolume,
      'ftue-2-voice-input-fader': jamClient.FTUESetOutputVolume,
      'ftue-audio-input-fader': jamClient.FTUESetInputVolume,
      'ftue-voice-input-fader': jamClient.FTUESetChatInputVolume,
      'ftue-audio-output-fader': jamClient.FTUESetOutputVolume
    };

    var faderReadMap = {
      'ftue-2-audio-input-fader': jamClient.FTUEGetInputVolume,
      'ftue-2-voice-input-fader': jamClient.FTUEGetOutputVolume,
      'ftue-audio-input-fader': jamClient.FTUEGetInputVolume,
      'ftue-voice-input-fader': jamClient.FTUEGetChatInputVolume,
      'ftue-audio-output-fader': jamClient.FTUEGetOutputVolume
    };

    function latencyTimeoutCheck() {
      if (context.JK.FtueWizard.latencyTimeout) {
        jamClient.FTUERegisterLatencyCallback('');
        context.JK.app.setWizardStep("5");
      }
    }

    function afterHide(data) {
      // Unsubscribe from FTUE VU callbacks.
      jamClient.FTUERegisterVUCallbacks('', '', '');

      if (!successfulFtue && app.cancelFtue) {
        app.cancelFtue();
        app.afterFtue = null;
        app.cancelFtue = null;
      }

    }

    function beforeShow(data) {
      successfulFtue = false;
      var vuMeters = [
        '#ftue-2-audio-input-vu-left',
        '#ftue-2-audio-input-vu-right',
        '#ftue-2-voice-input-vu-left',
        '#ftue-2-voice-input-vu-right',
        '#ftue-audio-input-vu-left',
        '#ftue-audio-input-vu-right',
        '#ftue-voice-input-vu-left',
        '#ftue-voice-input-vu-right',
        '#ftue-audio-output-vu-left',
        '#ftue-audio-output-vu-right'
      ];
      $.each(vuMeters, function () {
        context.JK.VuHelpers.renderVU(this,
          {vuType: "horizontal", lightCount: 12, lightWidth: 15, lightHeight: 3});
      });

      var faders = context._.keys(faderMap);
      $.each(faders, function () {
        var fid = this;
        context.JK.FaderHelpers.renderFader('#' + fid,
          {faderId: fid, faderType: "horizontal", width: 163});
        context.JK.FaderHelpers.subscribe(fid, faderChange);
      });
    }

    function afterShow(data) {

    }

    // renders volumes based on what the backend says
    function renderVolumes() {
      $.each(context._.keys(faderReadMap), function (index, faderId) {
        // faderChange takes a value from 0-100
        var $fader = $('[fader-id="' + faderId + '"]');

        var db = faderReadMap[faderId]();
        var faderPct = db + 80;
        context.JK.FaderHelpers.setHandlePosition($fader, faderPct);
        //faderChange(faderId, faderPct);
      });
    }

    function faderChange(e, data) {

    }

    function setSaveButtonState($save, enabled) {
      if (enabled) {
        $save.removeClass('disabled');
      }
      else {
        $save.addClass('disabled');
      }
    }

    function checkValidStateForTesting() {
      var reqMissing = !musicInAndOutSet() || currentAudioDriverId == null || currentAudioDriverId == '';

      if (reqMissing) {
        renderDisableTest();
      }
      else {
        renderEnableTest();
      }
    }

    function renderDisableTest() {
      $('#btn-ftue-test').addClass('disabled');
    }

    function renderEnableTest() {
      $('#btn-ftue-test').removeClass('disabled');
    }

    function renderStartNewFtueLatencyTesting() {
      setSaveButtonState($('#btn-ftue-2-save'), false);
    }

    function renderStopNewFtueLatencyTesting() {

    }

    function settingsInit() {
      jamClient.FTUEInit();
      //setLevels(0);
      resetFtueLatencyView();
      setSaveButtonState($('#btn-ftue-2-save'), false);
      if (jamClient.GetOSAsString() !== "Win32") {
        $('#btn-ftue-2-asio-control-panel').hide();
      }
      renderDisableTest();

      // Always reset the driver select box to "Choose..." which forces everything
      // to sync properly when the user reselects their driver of choice.
      // VRFS-375 and VRFS-561
      $('[layout-wizard="ftue"] [layout-wizard-step="0"] .settings-2-device select').val("");
      $('[layout-wizard="ftue"] [layout-wizard-step="0"] .settings-2-voice select').val("");
      $('[layout-wizard="ftue"] [layout-wizard-step="0"] #ftue-2-asio-framesize').val("").easyDropDown('disable');
      $('[layout-wizard="ftue"] [layout-wizard-step="0"] #ftue-2-asio-input-latency').val("0").easyDropDown('disable');
      $('[layout-wizard="ftue"] [layout-wizard-step="0"] #ftue-2-asio-output-latency').val("0").easyDropDown('disable');

      // with the old ftue, this is pretty annoying to reset these everytime
      $('[layout-wizard="ftue"] [layout-wizard-step="2"] .asio-settings .settings-driver select').val("");
      $('[layout-wizard="ftue"] [layout-wizard-step="2"] .settings-controls select[data-device="audio-input"]').val("");
      $('[layout-wizard="ftue"] [layout-wizard-step="2"] .settings-controls select[data-device="audio-output"]').val("");
      $('[layout-wizard="ftue"] [layout-wizard-step="2"] .settings-controls select[data-device="voice-chat-output"]').val("");
    }

    function setLevels(db) {
      if (db < -80 || db > 20) {
        throw ("BUG! ftue.js:setLevels db arg must be between -80 and 20");
      }
      var trackIds = jamClient.SessionGetIDs();
      var controlStates = jamClient.SessionGetControlState(trackIds);
      $.each(controlStates, function (index, value) {
        context.JK.Mixer.fillTrackVolume(value, false);
        // Default input/output to 0 DB
        context.trackVolumeObject.volL = db;
        context.trackVolumeObject.volR = db;
        jamClient.SessionSetControlState(trackIds[index]);
      });
      $.each(context._.keys(faderMap), function (index, faderId) {
        // faderChange takes a value from 0-100
        var $fader = $('[fader-id="' + faderId + '"]');

        var faderPct = db + 80;
        context.JK.FaderHelpers.setHandlePosition($fader, faderPct);
        faderChange(faderId, faderPct);
      });
    }

    function testComplete() {
      logger.debug("Test complete");
      var latencyMS = context.JK.FtueWizard.latencyMS;
      var ftueSucceeded = latencyMS <= 20;
      if (ftueSucceeded) {
        logger.debug(latencyMS + " is <= 20. Setting FTUE status to true");
        ftueSave(true); // Save the profile
        context.jamClient.FTUESetStatus(true); // No FTUE wizard next time
        rest.userCertifiedGear({success: true});

        // notify anyone curious about how it went
        $('div[layout-id=ftue]').trigger('ftue_success');
      }
      else {
        rest.userCertifiedGear({success: false, reason: "latency=" + latencyMS});
      }

      updateGauge();
    }

    function updateGauge() {
      var latencyMS = context.JK.FtueWizard.latencyMS;
      // Round to 2 decimal places
      latencyMS = (Math.round(latencyMS * 100)) / 100;
      logger.debug("Latency Value: " + latencyMS);
      if (latencyMS > 20) {
        $('#ftue-latency-congrats').hide();
        $('#ftue-latency-fail').show();
      } else {
        $('#ftue-latency-ms').html(latencyMS);
        $('#ftue-latency-congrats').show();
        $('#ftue-latency-fail').hide();
        if (latencyMS <= 10) {
          $('[layout-wizard-step="6"] .btnHelp').hide();
          $('[layout-wizard-step="6"] .btnRepeat').hide();
        }
      }
      setNeedleValue(latencyMS);
    }

    // Function to calculate an absolute value and an absolute value range into
    // a number of degrees on a circualar "speedometer" gauge. The 0 degrees value
    // points straight up to the middle of the real-world value range.
    // Arguments:
    //   value: The real-world value (e.g. 20 milliseconds)
    //   minValue: The real-world minimum value (e.g. 0 milliseconds)
    //   maxValue: The real-world maximum value (e.g. 40 milliseconds)
    //   degreesUsed: The number of degrees used to represent the full real-world
    //                range. 360 would be the entire circle. 270 would be 3/4ths
    //                of the circle. The unused gap will be at the bottom of the
    //                gauge.
    //               (e.g. 300)
    function degreesFromRange(value, minValue, maxValue, degreesUsed) {
      if (value > maxValue) {
        value = maxValue;
      }
      if (value < minValue) {
        value = minValue;
      }
      var range = maxValue - minValue;
      var floatVal = value / range;
      var degrees = Math.round(floatVal * degreesUsed);
      degrees -= Math.round(degreesUsed / 2);
      if (degrees < 0) {
        degrees += 360;
      }
      return degrees;
    }

    // Given a number of MS, and assuming the gauge has a range from
    // 0 to 40 ms. Update the gauge to the proper value.
    function setNeedleValue(ms) {
      logger.debug("setNeedleValue: " + ms);
      var deg = degreesFromRange(ms, 0, 40, 300);

      // Supporting Firefix, Chrome, Safari, Opera and IE9+.
      // Should we need to support < IE9, this will need more work
      // to compute the matrix transformations in those browsers -
      // and I don't believe they support transparent PNG graphic
      // rotation, so we'll have to change the needle itself.
      var css = {
        //"behavior":"url(-ms-transform.htc)",
        /* Firefox */
        "-moz-transform": "rotate(" + deg + "deg)",
        /* Safari and Chrome */
        "-webkit-transform": "rotate(" + deg + "deg)",
        /* Opera */
        "-o-transform": "rotate(" + deg + "deg)",
        /* IE9 */
        "-ms-transform": "rotate(" + deg + "deg)"
        /* IE6,IE7 */
        //"filter": "progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11=0.7071067811865476, M12=-0.7071067811865475, M21=0.7071067811865475, M22=0.7071067811865476)",
        /* IE8 */
        //"-ms-filter": '"progid:DXImageTransform.Microsoft.Matrix(SizingMethod=\'auto expand\', M11=0.7071067811865476, M12=-0.7071067811865475, M21=0.7071067811865475, M22=0.7071067811865476)"'
      };

      $('#ftue-latency-numerical').html(ms);
      $('#ftue-latency-needle').css(css);

    }

    function testLatency() {
      // we'll just register for call back right here and unregister in the callback.
      context.JK.FtueWizard.latencyTimeout = true;
      var cbFunc = 'JK.ftueLatencyCallback';
      logger.debug("Registering latency callback: " + cbFunc);
      jamClient.FTUERegisterLatencyCallback('JK.ftueLatencyCallback');
      var now = new Date();
      logger.debug("Starting Latency Test..." + now);
      context.setTimeout(latencyTimeoutCheck, 300 * 1000); // Timeout to 5 minutes
      jamClient.FTUEStartLatency();
    }

    function openASIOControlPanel(evt) {
      if (win32) {
        logger.debug("Calling FTUEOpenControlPanel()");
        jamClient.FTUEOpenControlPanel();
      }
    }

    function asioResync(evt) {
      jamClient.FTUERefreshDevices();
      ftueSave(false);
    }

    function ftueSave(persist) {
      // Explicitly set inputs and outputs to dropdown values
      // before save as the client seems to want this on changes to
      // things like frame size, etc..
      var $audioSelects = $('[layout-wizard-step="2"] .settings-controls select');
      $.each($audioSelects, function (index, value) {
        var $select = $(value);
        setAudioDevice($select);
      });
      if (musicInAndOutSet()) {

        renderVolumes();

        // If there is no voice-chat-input selected, let the back-end know
        // that we're using music for voice-chat.
        if ($('[layout-wizard-step="2"] select[data-device="voice-chat-input"]').val()) {
          // Voice input selected
          jamClient.TrackSetChatEnable(true);
        } else {
          // No voice input selected.
          jamClient.TrackSetChatEnable(false);
        }

        setDefaultInstrumentFromProfile();

        logger.debug("Calling FTUESave(" + persist + ")");
        var response = jamClient.FTUESave(persist);
        //setLevels(0);
        if (response) {
          logger.warn(response);
          // TODO - we may need to do something about errors on save.
          // per VRFS-368, I'm hiding the alert, and logging a warning.
          // context.alert(response);
        }
      } else {
        logger.debug("Aborting FTUESave as we need input + output selected.");
      }
    }

    function setAsioFrameSize(evt) {
      var val = parseFloat($(evt.currentTarget).val(), 10);
      if (isNaN(val)) {
        return;
      }
      logger.debug("Calling FTUESetFrameSize(" + val + ")");
      jamClient.FTUESetFrameSize(val);
      ftueSave(false);
    }

    function setAsioInputLatency(evt) {
      var val = parseInt($(evt.currentTarget).val(), 10);
      if (isNaN(val)) {
        return;
      }
      logger.debug("Calling FTUESetInputLatency(" + val + ")");
      jamClient.FTUESetInputLatency(val);
      ftueSave(false);
    }

    function setAsioOutputLatency(evt) {
      var val = parseInt($(evt.currentTarget).val(), 10);
      if (isNaN(val)) {
        return;
      }
      logger.debug("Calling FTUESetOutputLatency(" + val + ")");
      jamClient.FTUESetOutputLatency(val);
      ftueSave(false);
    }

    function testRequested(evt) {
      evt.preventDefault();
      var $testButton = $('#btn-ftue-test');
      if (!$testButton.hasClass('disabled')) {
        app.setWizardStep(3);
      }
      return false;
    }

    function videoLinkClicked(evt) {
      var myOS = jamClient.GetOSAsString();
      var link;
      if (myOS === 'MacOSX') {
        link = $(evt.currentTarget).attr('external-link-mac');
      } else if (myOS === 'Win32') {
        link = $(evt.currentTarget).attr('external-link-win');
      }
      if (link) {
        context.jamClient.OpenSystemBrowser(link);
      }
    }

    function events() {
      $('.ftue-video-link').hover(
        function (evt) { // handlerIn
          $(this).addClass('hover');
        },
        function (evt) { // handlerOut
          $(this).removeClass('hover');
        }
      );
      $('.ftue-video-link').on('click', videoLinkClicked);
      $('[layout-wizard-step="2"] .settings-driver select').on('change', audioDriverChanged);
      $('[layout-wizard-step="2"] .settings-controls select').on('change', audioDeviceChanged);
      $('#btn-asio-control-panel').on('click', openASIOControlPanel);
      $('#btn-asio-resync').on('click', asioResync);
      $('#asio-framesize').on('change', setAsioFrameSize);
      $('#asio-input-latency').on('change', setAsioInputLatency);
      $('#asio-output-latency').on('change', setAsioOutputLatency);
      $('#btn-ftue-test').on('click', testRequested)

      // New FTUE events
      $('.ftue-new .settings-2-device select').on('change', newFtueAudioDeviceChanged);
      $('.ftue-new .settings-2-voice select').on('change', newFtueAudioDeviceChanged);
      $('#btn-ftue-2-asio-resync').on('click', newFtueAsioResync);
      $('#btn-ftue-2-asio-control-panel').on('click', openASIOControlPanel);
      $('#ftue-2-asio-framesize').on('change', newFtueSetAsioFrameSize);
      $('#ftue-2-asio-input-latency').on('change', newFtueSetAsioInputLatency);
      $('#ftue-2-asio-output-latency').on('change', newFtueSetAsioOutputLatency);
      $('#btn-ftue-2-save').on('click', newFtueSaveSettingsHandler);
    }

    /**
     * This function loads the available audio devices from jamClient, and
     * builds up the select dropdowns in the audio-settings step of the FTUE wizard.
     */
    function loadAudioDevices() {
      var funcs = [
        jamClient.FTUEGetMusicInputs,
        jamClient.FTUEGetChatInputs,
        jamClient.FTUEGetMusicOutputs
      ];
      var selectors = [
        '[layout-wizard-step="2"] .audio-input select',
        '[layout-wizard-step="2"] .voice-chat-input select',
        '[layout-wizard-step="2"] .audio-output select'
      ];
      var optionsHtml = '';
      var deviceOptionFunc = function (deviceKey, index, list) {
        optionsHtml += '<option title="' + devices[deviceKey] + '" value="' + deviceKey + '">' + devices[deviceKey] + '</option>';
      };
      for (var i = 0; i < funcs.length; i++) {
        optionsHtml = '<option selected="selected" value="">Choose...</option>';

        var devices = funcs[i](); // returns hash of device id: device name
        var $select = $(selectors[i]);
        $select.empty();
        var sortedDeviceKeys = context._.keys(devices).sort();
        context._.each(sortedDeviceKeys, deviceOptionFunc);
        $select.html(optionsHtml);
        context.JK.dropdown($select);
        $select.removeAttr("disabled").easyDropDown('enable');
        $('[layout-wizard-step="2"] .settings-asio select').removeAttr("disabled").easyDropDown('enable')
        // Set selects to lowest possible values to start:
        $('#asio-framesize').val('2.5').change();
        $('#asio-input-latency').val('0').change();
        $('#asio-output-latency').val('0').change();
        // Special-case for a non-ASIO device, set to 1
        if (jamClient.GetOSAsString() === "Win32") { // Limit this check to Windows only.
          if (!(jamClient.FTUEHasControlPanel())) {
            $('#asio-input-latency').val('1').change();
            $('#asio-output-latency').val('1').change();
          }
        }
      }
    }

    /**
     * Load available drivers and populate the driver select box.
     */
    function loadAudioDrivers() {
      var drivers = context.jamClient.FTUEGetDevices(false);
      var chatDrivers = jamClient.FTUEGetChatInputs(false, false);
      var optionsHtml = '<option selected="selected" value="">Choose...</option>';
      var chatOptionsHtml = '<option selected="selected" value="">Choose...</option>';


      var driverOptionFunc = function (driverKey, index, list) {
        if(!drivers[driverKey]) {
          logger.debug("skipping unknown device:", driverKey)
        }
        else {
          optionsHtml += '<option title="' + drivers[driverKey] + '" value="' + driverKey + '">' + drivers[driverKey] + '</option>';
        }
      };

      var chatOptionFunc = function (driverKey, index, list) {
        chatOptionsHtml += '<option value="' + driverKey + '">' + chatDrivers[driverKey]+ '</option>';
      };

      var selectors = [
        '[layout-wizard-step="0"] .settings-2-device select',
        '[layout-wizard-step="2"] .settings-driver select'
      ];

      // handle standard devices
      var sortedDeviceKeys = context._.keys(drivers).sort();
      context._.each(sortedDeviceKeys, driverOptionFunc);
      $.each(selectors, function (index, selector) {
        var $select = $(selector);
        $select.empty();
        $select.html(optionsHtml);
        context.JK.dropdown($select);
      });

      selectors = ['[layout-wizard-step="0"] .settings-2-voice select'];
      var sortedVoiceDeviceKeys = context._.keys(chatDrivers).sort();

      // handle voice inputs
      context._.each(sortedVoiceDeviceKeys, chatOptionFunc);
      $.each(selectors, function (index, selector) {
        var $select = $(selector);
        $select.empty();
        $select.html(chatOptionsHtml);
        context.JK.dropdown($select);
      });

    }

    /** Once a configuration is decided upon, we set the user's default instrument based on data from their profile */
    function setDefaultInstrumentFromProfile() {
      var defaultInstrumentId;
      if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) {
        defaultInstrumentId = context.JK.instrument_id_to_instrument[context.JK.userMe.instruments[0].instrument_id].client_id;
      }
      else {
        defaultInstrumentId = context.JK.server_to_client_instrument_map['Other'].client_id;
      }

      jamClient.TrackSetInstrument(1, defaultInstrumentId);
    }

    /**
     * Handler for the new FTUE save button.
     */
    function newFtueSaveSettingsHandler(evt) {
      evt.preventDefault();
      var $saveButton = $('#btn-ftue-2-save');
      if ($saveButton.hasClass('disabled')) {
        return;
      }
      var selectedAudioDevice = $('.ftue-new .settings-2-device select').val();
      if (!(selectedAudioDevice)) {
        app.notify({
          title: "Please select an audio device",
          text: "Please choose a usable audio device, or select cancel."
        });
        return false;
      }

      setDefaultInstrumentFromProfile();

      logger.debug("Calling FTUESave(" + true + ")");
      jamClient.FTUESave(true);
      jamClient.FTUESetStatus(true); // No FTUE wizard next time
      rest.userCertifiedGear({success: true});
      // notify anyone curious about how it went
      $('div[layout-id=ftue]').trigger('ftue_success');
      successfulFtue = true; // important to before closeDialog
      app.layout.closeDialog('ftue');
      if (app.afterFtue) {
        // If there's a function to invoke, invoke it.
        app.afterFtue();
        app.afterFtue = null;
        app.cancelFtue = null;
      }
      return false;
    }

    // used in a .change event in a select to allow the select to reliably close
    // needed because ftue testing blocks the UI
    function releaseDropdown(proc) {
      setTimeout(function () {
        proc()
      }, 1)
    }

    // Handler for when the audio device is changed in the new FTUE screen
    // This works differently from the old FTUE. There is no input/output selection,
    // as soon as the user chooses a driver, we auto-assign inputs and outputs.
    // We also call jamClient.FTUEGetExpectedLatency, which returns a structure like:
    // { latency: 11.1875, latencyknown: true, latencyvar: 1}
    function newFtueAudioDeviceChanged(evt) {

      releaseDropdown(function () {
        renderStartNewFtueLatencyTesting();
        var $select = $(evt.currentTarget);

        var $audioSelect = $('.ftue-new .settings-2-device select');
        var $voiceSelect = $('.ftue-new .settings-2-voice select');
        var audioDriverId = $audioSelect.val();
        var voiceDriverId = $voiceSelect.val();
        jamClient.FTUESetMusicDevice(audioDriverId);
        jamClient.FTUESetChatInput(voiceDriverId);
        if (voiceDriverId) { // Let the back end know whether a voice device is selected
          jamClient.TrackSetChatEnable(true);
        } else {
          jamClient.TrackSetChatEnable(false);
        }
        if (!audioDriverId) {
          console.log("no audio driver ID");
          renderStopNewFtueLatencyTesting();
          // reset back to 'Choose...'
          newFtueEnableControls(false);
          return;
        }
        var musicInputs = jamClient.FTUEGetMusicInputs();
        var musicOutputs = jamClient.FTUEGetMusicOutputs();

        // set the music input to the first available input,
        // and output to the first available output
        var kin = null, kout = null, k = null;
        // TODO FIXME - this jamClient call returns a dictionary.
        // It's difficult to know what to auto-choose.
        // For example, with my built-in audio, the keys I get back are
        // digital in, line in, mic in and stereo mix. Which should we pick for them?
        for (k in musicInputs) {
          kin = k;
          break;
        }
        for (k in musicOutputs) {
          kout = k;
          break;
        }
        var result;
        if (kin && kout) {
          jamClient.FTUESetMusicInput(kin);
          jamClient.FTUESetMusicOutput(kout);
        } else {
          // TODO FIXME - how to handle a driver selection where we are unable to
          // autoset both inputs and outputs? (I'd think this could happen if either
          // the input or output side returned no values)
          renderStopNewFtueLatencyTesting();
          console.log("invalid kin/kout %o/%o", kin, kout);
          return;
        }

        newFtueUpdateLatencyView('loading');

        newFtueEnableControls(true);
        newFtueOsSpecificSettings();
        // make sure whatever the user sees in the frontend is what the backend thinks
        // this is necesasry because if you do a FTUE pass, close the client, and pick the same device
        // the backend will *silently* use values from before, because the frontend does not query the backend
        // for these values anywhere.

        // setting all 3 of these cause 3 FTUE's.  Instead, we set batchModify to true so that they don't call FtueSave(false), and call later ourselves
        batchModify = true;
        newFtueAsioFrameSizeToBackend($('#ftue-2-asio-framesize'));
        newFtueAsioInputLatencyToBackend($('#ftue-2-asio-input-latency'));
        newFtueAsioOutputLatencyToBackend($('#ftue-2-asio-output-latency'));
        batchModify = false;

        //setLevels(0);
        renderVolumes();

        logger.debug("Calling FTUESave(" + false + ")");
        jamClient.FTUESave(false)
        pendingFtueSave = false; // this is not really used in any real fashion.  just setting back to false due to batch modify above

        setVuCallbacks();

        var latency = jamClient.FTUEGetExpectedLatency();
        console.log("FTUEGetExpectedLatency: %o", latency);
        newFtueUpdateLatencyView(latency);
      });
    }

    function newFtueSave(persist) {
      logger.debug("newFtueSave persist(" + persist + ")")
      newFtueUpdateLatencyView('loading');
      logger.debug("Calling FTUESave(" + persist + ")");
      jamClient.FTUESave(persist);
      var latency = jamClient.FTUEGetExpectedLatency();
      newFtueUpdateLatencyView(latency);
    }

    function newFtueAsioResync(evt) {
      // In theory, we should be calling the following, but it causes
      // us to not have both inputs/outputs loaded, and simply calling
      // FTUE Save appears to resync things.
      //jamClient.FTUERefreshDevices();
      newFtueSave(false);
    }

    // simply tells backend what frontend shows in the UI
    function newFtueAsioFrameSizeToBackend($input) {
      var val = parseFloat($input.val(), 10);
      if (isNaN(val)) {
        logger.warn("unable to get value from framesize input: %o", $input.val());
        return false;
      }

      // we need to help WDM users start with good starting input/output values.
      if(isWDM()) {
        var defaultInput  = 1;
        var defaultOutput = 1;
        if(val == 2.5) {
          defaultInput    = 1;
          defaultOutput   = 1;
        }
        else if(val == 5) {
          defaultInput    = 3;
          defaultOutput   = 2;
        }
        else if(val == 10) {
          defaultInput    = 6;
          defaultOutput   = 5;
        }

        $('#ftue-2-asio-input-latency').val(defaultInput.toString());
        $('#ftue-2-asio-output-latency').val(defaultOutput.toString());

        logger.debug("Defaulting WDM input/output");
        logger.debug("Calling FTUESetInputLatency(" + defaultInput + ")");
        jamClient.FTUESetInputLatency(defaultInput);
        logger.debug("Calling FTUESetOutputLatency(" + defaultOutput + ")");
        jamClient.FTUESetOutputLatency(defaultOutput);
      }

      logger.debug("Calling FTUESetFrameSize(" + val + ")");
      jamClient.FTUESetFrameSize(val);
      return true;
    }

    function newFtueAsioInputLatencyToBackend($input) {
      var val = parseInt($input.val(), 10);
      if (isNaN(val)) {
        logger.warn("unable to get value from input latency input: %o", $input.val());
        return false;
      }
      logger.debug("Calling FTUESetInputLatency(" + val + ")");
      jamClient.FTUESetInputLatency(val);
      return true;
    }

    function newFtueAsioOutputLatencyToBackend($input) {
      var val = parseInt($input.val(), 10);
      if (isNaN(val)) {
        logger.warn("unable to get value from output latency input: %o", $input.val());
        return false;
      }
      logger.debug("Calling FTUESetOutputLatency(" + val + ")");
      jamClient.FTUESetOutputLatency(val);
      return true;
    }

    function newFtueSetAsioFrameSize(evt) {
      releaseDropdown(function () {
        renderStartNewFtueLatencyTesting();


        if (!newFtueAsioFrameSizeToBackend($(evt.currentTarget))) {
          renderStopNewFtueLatencyTesting();
          return;
        }
        if (batchModify) {
          pendingFtueSave = true;
        } else {
          newFtueSave(false);
        }
      });
    }

    function newFtueSetAsioInputLatency(evt) {
      releaseDropdown(function () {
        renderStartNewFtueLatencyTesting();

        if (!newFtueAsioInputLatencyToBackend($(evt.currentTarget))) {
          renderStopNewFtueLatencyTesting();
          return;
        }
        if (batchModify) {
          pendingFtueSave = true;
        } else {
          newFtueSave(false);
        }
      });
    }

    function newFtueSetAsioOutputLatency(evt) {
      releaseDropdown(function () {
        renderStartNewFtueLatencyTesting();

        if (!newFtueAsioOutputLatencyToBackend($(evt.currentTarget))) {
          renderStopNewFtueLatencyTesting();
          return;
        }
        if (batchModify) {
          pendingFtueSave = true;
        } else {
          newFtueSave(false);
        }
      });
    }

    // Enable or Disable the frame/buffer controls in the new FTUE screen
    function newFtueEnableControls(enable) {
      var $frame = $('#ftue-2-asio-framesize');
      var $bin = $('#ftue-2-asio-input-latency');
      var $bout = $('#ftue-2-asio-output-latency');
      if (enable) {
        $frame.removeAttr("disabled").easyDropDown('enable');
        $bin.removeAttr("disabled").easyDropDown('enable');
        $bout.removeAttr("disabled").easyDropDown('enable');
      } else {
        $frame.attr("disabled", "disabled").easyDropDown('disable');
        $bin.attr("disabled", "disabled").easyDropDown('disable');
        $bout.attr("disabled", "disabled".easyDropDown('disable'));
      }
    }

    function isWDM() {
      return jamClient.GetOSAsString() === "Win32" && !jamClient.FTUEHasControlPanel();
    }

    // Based on OS and Audio Hardware, set Frame/Buffer settings appropriately
    // and show/hide the ASIO button.
    function newFtueOsSpecificSettings() {
      var $frame = $('#ftue-2-asio-framesize');
      var $bin = $('#ftue-2-asio-input-latency');
      var $bout = $('#ftue-2-asio-output-latency');
      var $asioBtn = $('#btn-ftue-2-asio-control-panel');
      if (jamClient.GetOSAsString() === "Win32") {
        if (jamClient.FTUEHasControlPanel()) {
          // Win32 + ControlPanel = ASIO
          // frame=2.5, buffers=0
          $asioBtn.show();
          $frame.val('2.5');
          $bin.val('0');
          $bout.val('0');
        } else {
          // Win32, no ControlPanel = WDM/Kernel Streaming
          // frame=10, buffers=0
          $asioBtn.hide();
          $frame.val('2.5');
          $bin.val('1');
          $bout.val('1');
        }
      } else { // Assuming Mac. TODO: Linux check here
        // frame=2.5, buffers=0
        $asioBtn.hide();
        $frame.val('2.5');
        $bin.val('0');
        $bout.val('0');
      }

      // you need to give these to the backend now; if you don't, when the device is selected, the backend
      // may fail to open it (especially with kernel streaming).  Causes scary looking errors to user.
      newFtueAsioFrameSizeToBackend($('#ftue-2-asio-framesize'));
      newFtueAsioInputLatencyToBackend($('#ftue-2-asio-input-latency'));
      newFtueAsioOutputLatencyToBackend($('#ftue-2-asio-output-latency'));
    }

    function resetFtueLatencyView() {

      var $report = $('.ftue-new .latency .report');
      var $instructions = $('.ftue-new .latency .instructions');

      $('.ms-label', $report).empty();
      $('p', $report).empty();

      var latencyClass = 'start';

      $report.removeClass('good acceptable bad');
      $report.addClass(latencyClass);

      var instructionClasses = ['neutral', 'good', 'acceptable', 'bad', 'start', 'loading'];
      $.each(instructionClasses, function (idx, val) {
        $('p.' + val, $instructions).hide();
      });

      $('p.' + latencyClass, $instructions).show();
    }

    // Given a latency structure, update the view.
    function newFtueUpdateLatencyView(latency) {
      var $report = $('.ftue-new .latency .report');
      var $instructions = $('.ftue-new .latency .instructions');
      var latencyClass = "neutral";
      var latencyValue = "N/A";
      var $saveButton = $('#btn-ftue-2-save');
      if (latency && latency.latencyknown) {
        latencyValue = latency.latency;
        // Round latency to two decimal places.
        latencyValue = Math.round(latencyValue * 100) / 100;
        if (latency.latency <= 10) {
          latencyClass = "good";
          setSaveButtonState($saveButton, true);
        } else if (latency.latency <= 20) {
          latencyClass = "acceptable";
          setSaveButtonState($saveButton, true);
        } else {
          latencyClass = "bad";
          setSaveButtonState($saveButton, false);
        }
      } else {
        latencyClass = "unknown";
        setSaveButtonState($saveButton, false);
      }

      $('.ms-label', $report).html(latencyValue);
      $('p', $report).html('milliseconds');

      $report.removeClass('good acceptable bad unknown');
      $report.addClass(latencyClass);

      var instructionClasses = ['neutral', 'good', 'acceptable', 'unknown', 'bad', 'start', 'loading'];
      $.each(instructionClasses, function (idx, val) {
        $('p.' + val, $instructions).hide();
      });
      if (latency === 'loading') {
        $('p.loading', $instructions).show();
      } else {
        $('p.' + latencyClass, $instructions).show();
        renderStopNewFtueLatencyTesting();
      }
    }

    function audioDriverChanged(evt) {
      var $select = $(evt.currentTarget);
      currentAudioDriverId = $select.val();
      jamClient.FTUESetMusicDevice(currentAudioDriverId);
      loadAudioDevices();
      setAsioSettingsVisibility();
      checkValidStateForTesting();
    }

    function audioDeviceChanged(evt) {
      var $select = $(evt.currentTarget);
      setAudioDevice($select);
      if (musicInAndOutSet()) {
        ftueSave(false);
        setVuCallbacks();
      }
      checkValidStateForTesting();
    }

    function setAudioDevice($select) {
      var device = $select.data('device');
      var deviceId = $select.val();
      // Note: We always set, even on the "Choose" value of "", which clears
      // the current setting.
      var setFunction = deviceSetMap[device];
      setFunction(deviceId);
    }

    /**
     * Return a boolean indicating whether both the MusicInput
     * and MusicOutput devices are set.
     */
    function musicInAndOutSet() {
      var audioInput = $('[layout-wizard-step="2"] .audio-input select').val();
      var audioOutput = $('[layout-wizard-step="2"] .audio-output select').val();
      return (audioInput && audioOutput);
    }

    function setVuCallbacks() {
      jamClient.FTUERegisterVUCallbacks(
        "JK.ftueAudioOutputVUCallback",
        "JK.ftueAudioInputVUCallback",
        "JK.ftueChatInputVUCallback"
      );
      jamClient.SetVURefreshRate(200);
    }

    function setAsioSettingsVisibility() {
      logger.debug("jamClient.FTUEHasControlPanel()=" + jamClient.FTUEHasControlPanel());
      if (jamClient.FTUEHasControlPanel()) {
        logger.debug("Showing ASIO button");
        $('#btn-asio-control-panel').show();
      }
      else {
        logger.debug("Hiding ASIO button");
        $('#btn-asio-control-panel').hide();
      }
    }

    function initialize() {
      // If not on windows, hide ASIO settings
      if (jamClient.GetOSAsString() != "Win32") {
        logger.debug("Not on Win32 - modifying UI for Mac/Linux");
        win32 = false;
        $('[layout-wizard-step="2"] p[os="win32"]').hide();
        $('[layout-wizard-step="2"] p[os="mac"]').show();
        $('#btn-asio-control-panel').hide();
        $('[layout-wizard-step="2"] .settings-controls select').removeAttr("disabled");
        loadAudioDevices();
      }

      setAsioSettingsVisibility();

      events();
      var dialogBindings = { 'beforeShow': beforeShow,
        'afterShow': afterShow, 'afterHide': afterHide };
      app.bindDialog('ftue', dialogBindings);
      app.registerWizardStepFunction("0", settingsInit);
      app.registerWizardStepFunction("2", settingsInit);
      app.registerWizardStepFunction("4", testLatency);
      app.registerWizardStepFunction("6", testComplete);
      loadAudioDrivers();
    }

    // Expose publics
    this.initialize = initialize;

    // Expose degreesFromRange outside for testing
    this._degreesFromRange = degreesFromRange;

    return this;
  };


  // Common VU updater taking a dbValue (-80 to 20) and a CSS selector for the VU.
  context.JK.ftueVUCallback = function (dbValue, selector) {
    // Convert DB into a value from 0.0 - 1.0
    var floatValue = (dbValue + 80) / 100;
    context.JK.VuHelpers.updateVU($(selector), floatValue);
  };

  context.JK.ftueAudioInputVUCallback = function (dbValue) {
    context.JK.ftueVUCallback(dbValue, '#ftue-2-audio-input-vu-left');
    context.JK.ftueVUCallback(dbValue, '#ftue-2-audio-input-vu-right');
    context.JK.ftueVUCallback(dbValue, '#ftue-audio-input-vu-left');
    context.JK.ftueVUCallback(dbValue, '#ftue-audio-input-vu-right');
  };
  context.JK.ftueAudioOutputVUCallback = function (dbValue) {
    context.JK.ftueVUCallback(dbValue, '#ftue-audio-output-vu-left');
    context.JK.ftueVUCallback(dbValue, '#ftue-audio-output-vu-right');
  };
  context.JK.ftueChatInputVUCallback = function (dbValue) {
    context.JK.ftueVUCallback(dbValue, '#ftue-2-voice-input-vu-left');
    context.JK.ftueVUCallback(dbValue, '#ftue-2-voice-input-vu-right');
    context.JK.ftueVUCallback(dbValue, '#ftue-voice-input-vu-left');
    context.JK.ftueVUCallback(dbValue, '#ftue-voice-input-vu-right');
  };

  // Latency Callback
  context.JK.ftueLatencyCallback = function (latencyMS) {
    // We always show gauge screen if we hit this.
    // Clear out the 'timeout' variable.
    context.JK.FtueWizard.latencyTimeout = false;
    var now = new Date();
    console.log("ftueLatencyCallback: " + now);
    context.JK.FtueWizard.latencyMS = latencyMS;

    // Unregister callback:
    context.jamClient.FTUERegisterLatencyCallback('');
    // Go to 'congrats' screen -- although latency may be too high.
    context.JK.app.setWizardStep("6");
  };


})(window, jQuery);