/**
 * Common utility functions.
 */
(function (context, $) {

  "use strict";

  context.JK = context.JK || {};
  var gearUtils = {};
  var rest = new context.JK.Rest();
  context.JK.GearUtils = gearUtils;
  var logger = context.JK.logger;
  var ALERT_NAMES = context.JK.ALERT_NAMES;
  var ASSIGNMENT = context.JK.ASSIGNMENT;
  var VOICE_CHAT = context.JK.VOICE_CHAT;
  var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR;
  var EVENTS = context.JK.EVENTS;
  var SYSTEM_DEFAULT_PLAYBACK_ONLY = 'System Default (Playback Only)';
  var JAMKAZAM_VIRTUAL_INPUT = "JamKazam Virtual Input";


  context.JK.GearUtilsInstance = gearUtils;

  gearUtils.SKIPPED_NETWORK_TEST = -1; // we store a negative 1 to mean that we let the user skip.
  gearUtils.SYSTEM_DEFAULT_PLAYBACK_ONLY = SYSTEM_DEFAULT_PLAYBACK_ONLY;
  gearUtils.JAMKAZAM_VIRTUAL_INPUT = JAMKAZAM_VIRTUAL_INPUT;

  gearUtils.skippedNetworkTest = false; // we allow someone to play in session (for one client run) if it's our fault they can't network test score
  var reloadAudioTimeout = null;
  var currentAudioRestartLocation = null;

  // checks if it's an assigned OUTPUT or ASSIGNED CHAT
  gearUtils.isChannelAssigned = function (channel) {
    return channel.assignment == ASSIGNMENT.CHAT || channel.assignment == ASSIGNMENT.OUTPUT || channel.assignment > 0;
  }


  gearUtils.resyncAudio = function() {

    function backendMixerChange (e, data) {

      if (data.text == 'deferred') {
        context.JK.offBackendEvent(ALERT_NAMES.BACKEND_MIXER_CHANGE, 'gearUtilsResyncAudio', backendMixerChange)
        console.log("context.jamClient.FTUEGetExpectedLatency()", context.jamClient.FTUEGetExpectedLatency().latency)
        deferred.resolve();
      }
      ;
    }

    var deferred = new $.Deferred();

    context.JK.onBackendEvent(ALERT_NAMES.BACKEND_MIXER_CHANGE, 'gearUtilsResyncAudio', backendMixerChange);

    context.jamClient.SessionAudioResync();

    // give backend 5 seconds to timeout
    deferred.timer = setTimeout(function() {
      deferred.rejectWith('timeout');
      context.JK.offBackendEvent(ALERT_NAMES.BACKEND_MIXER_CHANGE, 'gearUtilsResyncAudio', backendMixerChange)
    }, 5000);

    return deferred;
  }


  // to play with others, you have to have inputs,
  // as well have a score below 20 ms
  gearUtils.canPlayWithOthers = function(profile) {


    var isNoInputProfile = gearUtils.isNoInputProfile(profile);
    var expectedLatency = context.jamClient.FTUEGetExpectedLatency();
    var audioLatency = expectedLatency ? expectedLatency.latency : null;

    // only compute high latency if the value is known. Also don't accept insanely largue values
    var highLatency = false;
    if(audioLatency) {
      highLatency = audioLatency > 20 && audioLatency < 1000;
    }

    var networkScore = context.jamClient.GetNetworkTestScore();
    var badNetworkScore = networkScore < 2;

    // for now, ignore latency
    if(gon.global.gear_check_ignore_high_latency) {
      highLatency = false
    }

    return {
      canPlay: !isNoInputProfile && !highLatency,
      isNoInputProfile: isNoInputProfile,
      badNetworkScore: badNetworkScore,
      highLatency: highLatency,
      audioLatency: audioLatency,
      networkScore: networkScore,
    }
  }

  gearUtils.isNoInputProfile = function(profile) {

    if (profile === undefined) {
      profile = context.jamClient.LastUsedProfileName();
    }

    return profile == SYSTEM_DEFAULT_PLAYBACK_ONLY;
  }

  gearUtils.canBeConfigured = function(profile) {
    return profile != SYSTEM_DEFAULT_PLAYBACK_ONLY;
  }

  gearUtils.createProfileName = function (deviceInfo, chatName) {
    var isSameInOut = deviceInfo.input.id == deviceInfo.output.id;

    var name = null;
    if (isSameInOut) {
      name = "In/Out: " + deviceInfo.input.info.displayName;
    }
    else {
      name = "In: " + deviceInfo.input.info.displayName + ", Out: " + deviceInfo.output.info.displayName
    }

    logger.debug("creating profile name: " + name);
    return name;
  }


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


    if (!audioInputDeviceId) {
      logger.debug("gearUtils.selectedDeviceInfo: no active input device");
      return null;
    }
    if (!audioOutputDeviceId) {
      logger.debug("gearUtils.selectedDeviceInfo: no active output device");
      return null;
    }

    if (!deviceInformation) {
      deviceInformation = gearUtils.loadDeviceInfo();
    }

    var input = deviceInformation[audioInputDeviceId];
    var output = deviceInformation[audioOutputDeviceId];

    var inputBehavior = AUDIO_DEVICE_BEHAVIOR[input.type];
    var outputBehavior = AUDIO_DEVICE_BEHAVIOR[output.type];

    return {
      input: {
        id: audioInputDeviceId,
        info: input,
        behavior: inputBehavior
      },
      output: {
        id: audioOutputDeviceId,
        info: output,
        behavior: outputBehavior
      }
    }
  }

  gearUtils.loadDeviceInfo = function () {

    var operatingSystem = context.JK.GetOSAsString();
    // should return one of:
    // * MacOSX_builtin
    // * MACOSX_interface
    // * Win32_wdm
    // * Win32_asio
    // * Win32_asio4all
    // * Linux
    function determineDeviceType(deviceId, displayName) {
      if (operatingSystem == "MacOSX") {
        if (displayName.toLowerCase().trim().indexOf("built-in") == 0) {
          return "MacOSX_builtin";
        }
        else {
          return "MacOSX_interface";
        }
      }
      else if (operatingSystem == "Win32") {
        if (context.jamClient.FTUEIsMusicDeviceWDM(deviceId)) {
          return "Win32_wdm";
        }
        else if (displayName.toLowerCase().indexOf("asio4all") > -1) {
          return "Win32_asio4all"
        }
        else {
          return "Win32_asio";
        }
      }
      else {
        return "Linux";
      }
    }

    var devices = context.jamClient.FTUEGetAudioDevices();
    //logger.debug("FTUEGetAudioDevices: " + JSON.stringify(devices));

    var loadedDevices = {};

    // augment these devices by determining their type
    context._.each(devices.devices, function (device) {

      if (device.name == "JamKazam Virtual Monitor") {
        return;
      }

      if (device.name == JAMKAZAM_VIRTUAL_INPUT) {
        return;
      }

      if ((device.name == 'Default Input' || device.name == 'Default Output') && operatingSystem == 'Win32'){
        console.log("dropping " + device.name)
        return;
      }

      var deviceInfo = {};

      deviceInfo.id = device.guid;
      deviceInfo.port_audio_name = device.port_audio_name;
      deviceInfo.type = determineDeviceType(device.guid, device.display_name);
      deviceInfo.displayType = AUDIO_DEVICE_BEHAVIOR[deviceInfo.type].display;
      deviceInfo.displayName = device.display_name;
      deviceInfo.inputCount = device.input_count;
      deviceInfo.outputCount = device.output_count;

      loadedDevices[device.guid] = deviceInfo;
    })

    logger.debug(context.JK.dlen(loadedDevices) + " devices loaded.", loadedDevices);

    return loadedDevices;
  }

  gearUtils.updateDefaultBuffers = function (selectedDeviceInfo, frameBuffers) {
    function hasWDMAssociated() {
      return selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')
    }

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

    // handle specific framesize settings
    if (hasWDMAssociated() || hasASIOAssociated()) {
      var framesize = frameBuffers.selectedFramesize();

      if (framesize == 2.5) {
        // if there is a WDM device, start off at 1/1 due to empirically observed issues with 0/0
        if (hasWDMAssociated()) {
          logger.debug("setting default buffers to 1/1");
          frameBuffers.setBufferIn('1');
          frameBuffers.setBufferOut('1');
        }
        else {
          // otherwise, it's ASIO, so go with 0/0
          logger.debug("setting default buffers to 0/0");
          frameBuffers.setBufferIn('0');
          frameBuffers.setBufferOut('0');
        }
      }
      else if (framesize == 5) {
        logger.debug("setting default buffers to 3/2");
        frameBuffers.setBufferIn('3');
        frameBuffers.setBufferOut('2');
      }
      else {
        logger.debug("setting default buffers to 2/2");
        frameBuffers.setBufferIn('2');
        frameBuffers.setBufferOut('2');
      }
    }
    else {
      logger.debug("setting default buffers to 0/0");
      frameBuffers.setBufferIn(0);
      frameBuffers.setBufferOut(0);
    }

    context.jamClient.FTUESetInputLatency(frameBuffers.selectedBufferIn());
    context.jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut());
  }

  gearUtils.ftueSummary = function (operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) {
    return {
      os: operatingSystem,
      version: context.jamClient.ClientUpdateVersion(),
      success: gearTest.isGoodFtue(),
      automated: isAutomated,
      score: {
        validLatencyScore: gearTest.isValidLatencyScore(),
        validIOScore: gearTest.isValidIOScore(),
        latencyScore: gearTest.getLatencyScore(),
        ioScore: gearTest.getIOScore(),
      },
      audioParameters: {
        frameSize: frameBuffers.selectedFramesize(),
        bufferIn: frameBuffers.selectedBufferIn(),
        bufferOut: frameBuffers.selectedBufferOut(),
      },
      devices: deviceInformation,
      selectedDevice: selectedDeviceInfo
    }
  }

  /**
   * Lists all profiles, but marks profiles good: true/false.
   * Also, current:true/false indicates which profile is active. (at most 1 profile will be marked current)
   * This is to provide a unified view of FTUEGetAllAudioConfigurations & FTUEGetGoodAudioConfigurations
   * @returns an array of profiles, where each profile is: {id: profile-name, good: boolean, class: 'bad' | 'good', current: boolean }
   */
  gearUtils.getProfiles = function () {
    var all = context.jamClient.FTUEGetAllAudioConfigurations();
    var good = context.jamClient.FTUEGetGoodAudioConfigurations();
    var current = context.jamClient.LastUsedProfileName();

    var profiles = [];
    context._.each(all, function (item) {

      profiles.push({id: item, good: false, class: 'bad', current: current == item})
    });

    if (good) {
      for (var i = 0; i < good.length; i++) {
        for (var j = 0; j < profiles.length; j++) {
          if (good[i] == profiles[j].id) {
            profiles[j].good = true;
            profiles[j].class = 'good';
            break;
          }
        }
      }
    }

    return profiles;
  }
  gearUtils.postDiagnostic = function (operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) {
    rest.createDiagnostic({
      type: 'GEAR_SELECTION',
      data: {
        client_type: context.JK.clientType(),
        client_id: context.JK.JamServer.clientID,
        summary: gearUtils.ftueSummary(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated)
      }
    });
  }

  // complete list of possibly chatInputs, whether currently assigned as the chat channel or not
  // each item should be {id: channelId, name: channelName, assignment: channel assignment}

  gearUtils.getChatInputs = function () {

    var musicPorts = jamClient.FTUEGetChannels();
    //var chatsOnCurrentDevice = context.jamClient.FTUEGetChatInputs(true);
    var chatsOnOtherDevices = context.jamClient.FTUEGetChatInputs(false, true);

    // remove all virtual/remote junk form chat inputs. Their key contains' JamKazam when it's Virtual Input or Remote
    Object.keys(chatsOnOtherDevices).forEach(function(key) {
      if(key.indexOf('JamKazam') > -1) {
        delete chatsOnOtherDevices[key]
      }
    } )

    // this wasapi logic is this:  if there are any non-WASAPI items, present only them, because WASAPI is very high latency and
    // highly desirable. But if the user ONLY has wasapi, then fine, we show them (by not deleting them from the list)
    var allWasapi = true;
    context._.each(chatsOnOtherDevices, function (chatChannelName, chatChannelId) {
        if(chatChannelName.indexOf('WASAPI') == -1) {
          allWasapi = false;
          return false
        }
    })
    if(!allWasapi) {
      Object.keys(chatsOnOtherDevices).forEach(function(key) {
        if(chatsOnOtherDevices[key].indexOf('WASAPI') > -1) {
         // delete chatsOnOtherDevices[key]
        }
      } )
    }

    var chatInputs = [];
    //context._.each(musicPorts.inputs, function(input) {
    //  chatInputs.push({id: input.id, name: input.name, assignment:input.assignment});
    //});

    var deDupper = {};

    context._.each(musicPorts.inputs, function (input) {

      var chatInput = {id: input.id, name: input.name, assignment: input.assignment};
      if (!deDupper[input.id]) {
        if (input.assignment <= 0) {
          chatInputs.push(chatInput);
          deDupper[input.id] = chatInput;
        }
      }
    });

    /**context._.each(chatsOnCurrentDevice, function(chatChannelName, chatChannelId) {
      var chatInput = {id: chatChannelId, name: chatChannelName, assignment: ASSIGNMENT.UNASSIGNED};
      if(!deDupper[chatInput.id]) {
        var assignment = context.jamClient.TrackGetAssignment(chatChannelId, true);
        if(assignment <= 0) {
            chatInputs.push(chatInput);
            deDupper[chatInput.id] = chatInput;
        }
      }
    })*/

    context._.each(chatsOnOtherDevices, function (chatChannelName, chatChannelId) {
      var chatInput = {id: chatChannelId, name: chatChannelName, assignment: null};
      if (!deDupper[chatInput.id]) {
        var assignment = context.jamClient.TrackGetAssignment(chatChannelId, true);
        chatInput.assignment = assignment;

        chatInputs.push(chatInput);
        deDupper[chatInput.id] = chatInput;
      }
    })

    return chatInputs;
  }

  gearUtils.isChannelAvailableForChat = function (chatChannelId, musicPorts) {
    var result = true;
    context._.each(musicPorts.inputs, function (inputChannel) {
      // if the channel is currently assigned to a track, it not unassigned
      if (inputChannel.id == chatChannelId && (inputChannel.assignment > 0)) {
        result = false;
        return false; // break
      }
    });

    return result;
  }

  // if the user has a good user network score, immediately returns with a resolved deferred object.
  // if not, the user will have the network test dialog prompted... once it's closed, then you'll be told reject() if score is still bad, or resolve() if now good
  gearUtils.guardAgainstBadNetworkScore = function (app) {
    var deferred = new $.Deferred();

    if (!gearUtils.validNetworkScore()) {
      // invalid network test score.  They have to score to move on
      app.layout.showDialog('network-test').one(EVENTS.DIALOG_CLOSED, function () {
        if (gearUtils.validNetworkScore()) {
          deferred.resolve();
        }
        else {
          deferred.reject();
        }
      });
    }
    else {
      deferred.resolve();
    }
    return deferred;
  }

  // XXX this isn't quite right... it needs to check if a good device is *active*
  // but seen too many problems so far with the backend not reporting any profile active
  gearUtils.hasGoodActiveProfile = function (verifyTracks) {
    var hasOneConfigureDevice = context.JK.hasOneConfiguredDevice();
    logger.debug("hasGoodActiveProfile: " + hasOneConfigureDevice ? "devices='has at least one configured device' " : "devices='has no configured device' ")
    return hasOneConfigureDevice;
  }

  // if the user does not have any profiles, show the FTUE
  gearUtils.guardAgainstInvalidGearConfiguration = function (app) {
    var deferred = new $.Deferred();

    if (context.jamClient.FTUEGetAllAudioConfigurations().length == 0) {
      app.layout.showDialog('gear-wizard').one(EVENTS.DIALOG_CLOSED, function () {
        if (gearUtils.hasGoodActiveProfile() && gearUtils.validNetworkScore()) {
          deferred.resolve();
        }
        else {
          deferred.reject();
        }
      });
    }
    else {
      deferred.resolve();
    }

    return deferred;
  }

  context.JK.guardAgainstSinglePlayerProfile = function(app, beforeCallback) {

    var deferred = new $.Deferred();

    var canPlayWithOthers = context.JK.GearUtilsInstance.canPlayWithOthers();

    if(!canPlayWithOthers.canPlay) {
      logger.debug("guarding against single player profile")

      var $dialog = app.layout.showDialog('single-player-profile-dialog');

      // so that callers can check dialog result
      canPlayWithOthers.dialog = $dialog;

      // allow callers to take action before default behavior
      if(beforeCallback) {
        $dialog.one(EVENTS.DIALOG_CLOSED, beforeCallback);
      }

      $dialog.one(EVENTS.DIALOG_CLOSED, function(e, data) {

        if(data.canceled) {
          deferred.rejectWith(canPlayWithOthers, [{ reason: 'canceled', controlled_location:false }])
        }
        else {
          if(data.result.choice == 'private_session') {
            var data = {
              createType: 'quick-start',
              timezone: {},
              recurring_mode: {},
              language: {},
              band: {},
              musician_access: {},
              fans_access: {},
              rsvp_slots: [],
              open_rsvps: false
            };

            context.JK.privateSessionSettings(data)

            context.JK.createSession(app, data)
              .done(function(response) {
                var sessionId = response.id;

                context.JK.GA.trackSessionCount(true, true, 0);

                deferred.rejectWith(canPlayWithOthers, [{ reason: 'private_session', controlled_location:true }])
                // we redirect to the session screen, which handles the REST call to POST /participants.
                logger.debug("joining session screen: " + sessionId)
                context.location = '/client#/session/' + sessionId;
              })
              .fail(function(jqXHR) {
                deferred.rejectWith(canPlayWithOthers, [{ reason: 'gear_setup', controlled_location:false }])
                logger.debug("unable to schedule a private session")
                app.notifyServerError(jqXHR, "Unable to schedule a private session");
              })
          }
          else if(data.result.choice == 'gear_setup') {
            deferred.rejectWith(canPlayWithOthers, [{ reason: data.result.choice,controlled_location:true }])
            window.location = '/client#/account/audio'
          }
          else
          {
            deferred.rejectWith(canPlayWithOthers, [{ reason: 'unknown', controlled_location:false }])
            logger.error("unknown choice: " + data.result.choice)
            alert("unknown choice: " + data.result.choice)
          }
        }
      })
    }
    else  {
      deferred.resolveWith(canPlayWithOthers)
    }


    return deferred;
  }

  gearUtils.guardAgainstActiveProfileMissing = function (app, backendInfo) {


    var deferred = new $.Deferred();
    logger.debug("guardAgainstActiveProfileMissing: backendInfo %o", backendInfo);
    if (backendInfo.error && backendInfo['reason'] == 'no_profile' && context.jamClient.FTUEGetAllAudioConfigurations().length > 0) {
      // if the backend says we have no_profile, but we have profiles , send them to the audio profile screen
      // this should be a very rare path
      deferred.reject({reason: 'handled', nav: '/client#/account/audio'});
      context.JK.Banner.showAlert('No Active Profile', 'We\'ve sent you to the audio profile screen to remedy the fact that you have no active audio profile. Please select ACTIVATE on an existing profile, or select ADD NEW GEAR to add a new profile.');
    }
    else if (backendInfo.error && backendInfo['reason'] == 'device_failure') {
      app.layout.showDialog('audio-profile-invalid-dialog')
        .one(EVENTS.DIALOG_CLOSED, function (e, data) {
          if (!data.result || data.result == 'cancel') {
            deferred.reject({reason: 'handled', nav: 'BACK'});
          }
          else if (data.result == 'configure_gear') {
            deferred.reject({reason: 'handled', nav: '/client#/account/audio'});
          }
          else if (data.result == 'session') {
            deferred.resolve();
          }
          else {
            logger.error("unknown result condition in audio-profile-invalid-dialog: " + data.result)
            deferred.reject()
          }
        });
    }
    else {
      deferred.resolve();
    }
    return deferred;
  }

  // tests both device config, and network score
  gearUtils.guardAgainstInvalidConfiguration = function (app, verifyNetworkScore) {
    var deferred = new $.Deferred();
    gearUtils.guardAgainstInvalidGearConfiguration(app)
      .fail(function () {
        deferred.reject();
      })
      .done(function () {
        if(verifyNetworkScore) {
          gearUtils.guardAgainstBadNetworkScore(app)
            .fail(function () {
              deferred.reject();
            })
            .done(function () {
              deferred.resolve();
            })
        }
        else {
          deferred.resolve();
        }

      })

    return deferred;
  }

  gearUtils.skipNetworkTest = function () {
    context.jamClient.SetNetworkTestScore(gearUtils.SKIPPED_NETWORK_TEST);
    gearUtils.skippedNetworkTest = true;
  }

  gearUtils.isNetworkTestSkipped = function () {
    return gearUtils.skippedNetworkTest;
  }

  gearUtils.validNetworkScore = function () {
    return !gon.global.network_test_required || gearUtils.skippedNetworkTest || context.jamClient.GetNetworkTestScore() >= 2;
  }

  gearUtils.isRestartingAudio = function () {
    return !!reloadAudioTimeout;
  }

  gearUtils.scheduleAudioRestart = function (location, initial_delay, beforeScan, afterScan, cancelScan) {

    logger.debug("scheduleAudioRestart: (from " + location + ")")

    var cancellable = true;

    function clearAudioReloadTimer() {

      if (!cancellable) {
        return;
      }

      if (cancelScan) {
        cancelScan();
      }
      else if (afterScan) {
        afterScan(true);
      }

      clearTimeout(reloadAudioTimeout);
      reloadAudioTimeout = null;
      currentAudioRestartLocation = null;
      cancellable = false;
    }

    // refresh timer if outstanding
    if (reloadAudioTimeout) {
      logger.debug("scheduleAudioRestart: clearing timeout (from " + location + ")")
      clearTimeout(reloadAudioTimeout);
    }

    currentAudioRestartLocation = location;

    if (beforeScan) {
      beforeScan();
    }

    reloadAudioTimeout = setTimeout(function () {
      logger.debug("scheduleAudioRestart: rescan beginning (from " + location + ")")
      reloadAudioTimeout = null;
      currentAudioRestartLocation = null;
      cancellable = false;

      if (afterScan) {
        afterScan(false);
      }
    }, initial_delay ? initial_delay : 5000);

    return clearAudioReloadTimer;
  }

  gearUtils.bootstrapDefaultPlaybackProfile = function () {

    var profiles = gearUtils.getProfiles();

    var foundSystemDefaultPlaybackOnly = false
    context._.each(profiles, function (profile) {
      if (profile.id == SYSTEM_DEFAULT_PLAYBACK_ONLY) {
        foundSystemDefaultPlaybackOnly = true
        return false;
      }
    })

    if (!foundSystemDefaultPlaybackOnly) {
      logger.debug("creating system default profile (playback only)")
      if(!gearUtils.createDefaultPlaybackOnlyProfile()) {
        logger.error("unable to create the default playback profile!");
      }
    }
  }
  gearUtils.createDefaultPlaybackOnlyProfile = function () {

    var eMixerInputSampleRate = {
      JAMKAZAM_AUTO_SR: 0,
      USE_DEVICE_DEFAULT_SR: 1,
      PREFER_44: 2,
      PREFER_48: 3,
      PREFER_96: 4
    }

    // null//upgrade protect
    if(context.jamClient.FTUECreateUpdatePlayBackProfile) {
      return context.jamClient.FTUECreateUpdatePlayBackProfile(SYSTEM_DEFAULT_PLAYBACK_ONLY,
        eMixerInputSampleRate.USE_DEVICE_DEFAULT_SR,
        0, // buffering
        false); // start audio
    }
    else {
      return false;
    }
  }

})(window, jQuery);