(function (context, $) {

  "use strict";

  context.JK = context.JK || {};
  context.JK.ConfigureTracksHelper = function (app) {
    var logger = context.JK.logger;
    var ASSIGNMENT = context.JK.ASSIGNMENT;
    var VOICE_CHAT = context.JK.VOICE_CHAT;
    var MAX_TRACKS = context.JK.MAX_TRACKS;
    var MAX_OUTPUTS = context.JK.MAX_OUTPUTS;
    var gearUtils = context.JK.GearUtils;

    var $parent                   = null;
    var $templateAssignablePort   = null;
    var $templateTrackTarget      = null;
    var $templateOutputTarget     = null;
    var $unassignedInputsHolder   = null;
    var $unassignedOutputsHolder  = null;
    var $tracksHolder             = null;
    var $outputChannelHolder      = null;
    var $instrumentsHolder        = null;
    var isDragging                = false;

    function removeHoverer($hoverChannel) {
      var $channel = $hoverChannel.data('original')
      $channel.data('cloned', null);
      $hoverChannel.remove();
    }

    function hoverIn($channel) {
      if(isDragging) return;

      var $container = $channel.closest('.target');
      var inTarget = $container.length > 0;
      if(!inTarget) {
        $container = $channel.closest('.channels-holder')
      }

      var $inputs = $container.find('.ftue-input');

      var index = $inputs.index($channel);
      //  $channel.css('padding', '0 5px');
      if(inTarget) {
        $channel.data('container', $container)
        $channel.addClass('hovering');
        $channel.css('color', 'white')
        $channel.css('background-color', '#333');
        $channel.css('border', '#333');
        $channel.css('border-radius', '2px');
        $channel.css('min-width', '49%');
        $channel.css('width', 'auto');
        $channel.css('position', 'absolute');
        $container.css('overflow', 'visible');
      }
      else {
        var $offsetParent = $channel.offsetParent();
        var parentOffset = $offsetParent.offset();

        var hoverChannel = $(context._.template($templateAssignablePort.html(), {id: 'bogus', name: $channel.text(), direction: 'bogus'}, { variable: 'data' }));
        hoverChannel
          .css('position', 'absolute')
          .css('color', 'white')
          .css('left', $channel.position().left)
          .css('top', $channel.position().top)
          .css('background-color', '#333')
          .css('min-width', $channel.width())
          .css('min-height', $channel.height())
          .css('z-index', 10000)
          .css('background-position', '4px 6px')
          .css('padding-left', '14px')
          .data('original', $channel);

        $channel.data('cloned', hoverChannel);
        hoverChannel
          .hover(function(e) {
            var hoverCheckTimeout = hoverChannel.data('hoverCheckTimeout');
            if(hoverCheckTimeout) {
              clearTimeout(hoverCheckTimeout);
              hoverChannel.data('hoverCheckTimeout', null);
            }
          }, function() { removeHoverer($(this)); })
          .mousedown(function(e) {
            // because we have obscured the element the user wants to drag,
            // we proxy a mousedown on the hover-element to the covered .ftue-input ($channel).
            // this causes jquery.drag to get going even though the user clicked a different element
            $channel.trigger(e)
          })
        hoverChannel.data('hoverCheckTimeout', setTimeout(function() {
          // check if element has already been left
          hoverChannel.data('hoverCheckTimeout', null);
          removeHoverer(hoverChannel);
        }, 500));
        hoverChannel.prependTo($offsetParent);
      }

      $channel.css('z-index', 10000)
      if(inTarget && $inputs.length == 2) {

        if(index == 0) {
          $channel.css('right', '50%')
        }
        else {
          $channel.css('left', '51%')
        }
      }

    }

    function hoverOut($channel) {

      var $cloned = $channel.data('cloned');
      if($cloned) {
        return; // let the cloned handle the rest of hover out logic when it's hovered-out
      }

      $channel
        .removeClass('hovering')
        .css('color', '')
        .css('font-weight', '')
        .css('position', '')
        .css('width', '')
        .css('background-color', '')
        .css('padding', '')
        .css('padding-left', '')
        .css('background-position', '')
        .css('border', '')
        .css('border-radius', '')
        .css('right', '')
        .css('left', '')
        .css('min-width', '')
        .css('z-index', '')
        .css('margin-left', '')
        .css('max-width', 'auto');

      //var $container = $channel.closest('.target');
      var $container = $channel.data('container');
      if($container) {
        $container.css('overflow', '')
      }
    }

    function fixClone($clone) {
      $clone
        .css('color', '')
        .css('font-weight', '')
        .css('width', 'auto')
        .css('background-color', '')
        .css('padding', '')
        .css('border', '')
        .css('border-radius', '')
        .css('right', '')
        .css('min-width', '')
    }

    // inputChannelFilter is an optional argument that is used by the Gear Wizard.
    // basically, if an input channel isn't in there, it's not going to be displayed
    function loadChannels(forceInputsToUnassign, inputChannelFilter) {
      var musicPorts = jamClient.FTUEGetChannels();

      $unassignedInputsHolder.empty();
      $unassignedOutputsHolder.empty();

      // reset all counts
      $tracksHolder.find('.track-target').attr('track-count', '0');
      $outputChannelHolder.find('.output-target').attr('output-count', '0');
      $tracksHolder.find('.ftue-input').remove();
      $outputChannelHolder.find('.ftue-input').remove();

      var inputChannels = musicPorts.inputs;
      var outputChannels = musicPorts.outputs;

      // uncomment to add a bunch of bogus channels
      //inputChannels = inputChannels.concat(inputChannels.concat(inputChannels.concat(inputChannels)))

      context._.each(inputChannels, function (inputChannel) {

        if(inputChannelFilter && !(inputChannelFilter.indexOf(inputChannel.id) > -1)) {
          // skipping input channel because it's not in the filter
          return;
        }

        var $channel = $(context._.template($templateAssignablePort.html(), $.extend({}, inputChannel, {direction:'in'}), { variable: 'data' }));

        $channel.hover(
          function() { hoverIn ($(this)) },
          function() { hoverOut($(this)) }
        );

        if(forceInputsToUnassign || inputChannel.assignment == ASSIGNMENT.UNASSIGNED) {
          unassignInputChannel($channel);
        }
        else if(inputChannel.assignment == ASSIGNMENT.CHAT) {
          // well, we can't show it as unused... if there were a place to show chat inputs, we would put it there.
          // but we don't have it, so just skip
          logger.debug("skipping channel ", inputChannel)
          return;
        }
        else {
          // find the track this belongs in

          var trackNumber = inputChannel.assignment - 1;

          var $track = $tracksHolder.find('.track[data-num="' + trackNumber + '"]')

          if($track.length == 0) {
            context.JK.alertSupportedNeeded('Unable to find a track for channel with assignment ' + inputChannel.assignment);
            return false;
          }
          addChannelToTrack($channel, $track.find('.track-target'));
        }

        $channel.draggable({
          helper: 'clone',
          start: function(e, ui) {
            isDragging = true;
            var $channel = $(this);
            fixClone(ui.helper);
            var $track = $channel.closest('.track-target');
            var isUnassigned = $track.length == 0;
            if(isUnassigned) {
              $tracksHolder.find('.track-target').addClass('possible-target');
            }
            else {
              $tracksHolder.find('.track-target').addClass('possible-target');
              $unassignedInputsHolder.addClass('possible-target');
            }
          },
          stop: function() {
            isDragging = false;
            $tracksHolder.find('.track-target').removeClass('possible-target');
            $unassignedInputsHolder.removeClass('possible-target')
          }
        });
      })

      var outputAssignment = 0;
      context._.each(outputChannels, function (outputChannel, index) {
        var $channel = $(context._.template($templateAssignablePort.html(), $.extend({}, outputChannel, {direction:'out'}), { variable: 'data' }));

        $channel.hover(
          function() { hoverIn ($(this)) },
          function() { hoverOut($(this)) }
        );

        if(outputChannel.assignment == ASSIGNMENT.UNASSIGNED) {
          unassignOutputChannel($channel);
        }
        else {
          var $output = $outputChannelHolder.find('.output[data-num="' + outputAssignment + '"]');
          outputAssignment++;

          if($output.length == 0) {
            context.JK.alertSupportedNeeded('Unable to find an output holder for channel: ' + outputChannel.id + ' with assignment ' + outputChannel.assignment);
            return false;
          }
          addChannelToOutput($channel, $output.find('.output-target'));
        }

        $channel.draggable({
          helper: 'clone',
          start: function(e,ui) {
            isDragging = true;
            var $channel = $(this);
            fixClone(ui.helper);
            var $output = $channel.closest('.output-target');
            var isUnassigned = $output.length == 0;
            if(isUnassigned) {
              $outputChannelHolder.find('.output-target').addClass('possible-target');
            }
            else {
              $outputChannelHolder.find('.output-target').addClass('possible-target');
              $unassignedOutputsHolder.addClass('possible-target');
            }
          },
          stop: function() {
            isDragging = false;
            $outputChannelHolder.find('.output-target').removeClass('possible-target');
            $unassignedOutputsHolder.removeClass('possible-target')
          }
        });
      });
    }

    // iterates through the dom and returns a pure data structure for track associations and output channels
    function getCurrentState() {

      var state = {};
      state.tracks = [];
      state.unassignedChannels = [];
      state.outputs = [];
      var $unassignedInputChannels = $unassignedInputsHolder.find('.ftue-input');
      var $unassignedOutputChannels = $unassignedOutputsHolder.find('.ftue-input');
      var $tracks = $tracksHolder.find('.track-target');
      var $outputs = $outputChannelHolder.find('.output-target');

      context._.each($unassignedInputChannels, function($unassignedInput) {
        $unassignedInput = $($unassignedInput);
        var channelId = $unassignedInput.attr('data-input-id');
        state.unassignedChannels.push(channelId);
      })

      context._.each($unassignedOutputChannels, function($unassignedOutput) {
        $unassignedOutput = $($unassignedOutput);
        var channelId = $unassignedOutput.attr('data-input-id');
        state.unassignedChannels.push(channelId);
      })

      context._.each($tracks, function($track, index) {
        $track = $($track);
        var $assignedChannels = $track.find('.ftue-input');

        var track = {index: index, channels:[]};
        context._.each($assignedChannels, function($assignedChannel) {
          $assignedChannel = $($assignedChannel);
          track.channels.push($assignedChannel.attr('data-input-id'))
        });

        // sparse array
        if(track.channels.length > 0) {
          state.tracks.push(track);
        }
        var $instrument = $instrumentsHolder.find('[data-num="' + index + '"]').find('.icon-instrument-select');
        track.instrument_id = $instrument.data('instrument_id');
      })

      context._.each($outputs, function($output, index) {
        $output = $($output);
        var $assignedChannel = $output.find('.ftue-input');

        // this is overkill since there should only be 1 or 0 .ftue-inputs in a given .output
        var outputSlot = {index: index, channels:[]};
        context._.each($assignedChannel, function($assignedChannel) {
          $assignedChannel = $($assignedChannel);
          outputSlot.channels.push($assignedChannel.attr('data-input-id'))
        });

        // sparse array
        if(outputSlot.channels.length > 0) {
          state.outputs.push(outputSlot);
        }
      })
      return state;
    }

    function validate(tracks) {
      // there must be at least one assigned channel
      if(tracks.tracks.length == 0) {
        logger.debug("ConfigureTracks validation error: must have assigned at least one input port to a track.");
        context.JK.Banner.showAlert('Must have assigned at least one input port to a track.');
        return false;
      }

      // there must be some instruments
      context._.each(tracks.tracks, function(track) {
        if(!track.instrument_id) {
          logger.debug("ConfigureTracks validation error: all tracks with ports assigned must specify an instrument.");
          context.JK.Banner.showAlert('Please use the instrument icons to choose what you plan to play on each track.');
          return false;
        }
      });

      // there must be exactly 2 output channels assigned
      if(tracks.outputs.length != 2 || (tracks.outputs[0].channels.length != 1 && track.outputs[1].channels.length != 1)) {
        logger.debug("ConfigureTracks validation error: must have assigned exactly two output ports");
        context.JK.Banner.showAlert('Must have assigned exactly 2 output ports.');
        return false;
      }

      return true;
    }

    function save(state) {

      context._.each(state.unassignedChannels, function(unassignedChannelId) {
        context.jamClient.TrackSetAssignment(unassignedChannelId, true, ASSIGNMENT.UNASSIGNED);
      });

      // save input/tracks
      context._.each(state.tracks, function(track, index) {

        var trackNumber = index + 1;

        context._.each(track.channels, function(channelId) {
          context.jamClient.TrackSetAssignment(channelId, true, trackNumber);

        });
        logger.debug("context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id)", trackNumber, track.instrument_id);
        context.jamClient.TrackSetInstrument(trackNumber, context.JK.instrument_id_to_instrument[track.instrument_id].client_id);
      });

      // save outputs
      context._.each(state.outputs, function(output, index) {
        context._.each(output.channels, function(channelId) {
          context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.OUTPUT);
        });
      });

      var result = context.jamClient.TrackSaveAssignments();

      if(!result || result.length == 0) {
        // success
        return true;
      }
      else {
        context.JK.Banner.showAlert('Unable to save assignments. ' + result);
        return false;
      }
    }

    function loadTrackInstruments(forceInputsToUnassign) {
      var $trackInstruments = $instrumentsHolder.find('.track-instrument');

      context._.each($trackInstruments, function(trackInstrument) {
        var $trackInstrument = $(trackInstrument);

        var trackIndex = parseInt($trackInstrument.attr('data-num')) + 1;

        var clientInstrument = context.jamClient.TrackGetInstrument(trackIndex);

        var instrument = context.JK.client_to_server_instrument_map[clientInstrument];

        $trackInstrument.instrumentSelectorSet(instrument ? instrument.server_id : instrument);
      });
    }

    function trySave() {
      var state = getCurrentState();

      if(!validate(state)) {
        return false;
      }

      var saved = save(state);

      if(saved) {
        context.JK.GA.trackConfigureTracksCompletion(context.JK.detectOS());
      }

      return saved;
    }

    function reset(forceInputsToUnassign, inputChannelFilter) {
      loadChannels(forceInputsToUnassign, inputChannelFilter);
      loadTrackInstruments(forceInputsToUnassign);
    }

    function unassignOutputChannel($channel) {
      var $originallyAssignedTrack = $channel.closest('.output-target');
      $unassignedOutputsHolder.append($channel);
      $originallyAssignedTrack.attr('output-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length);
    }

    function unassignInputChannel($channel) {
      var $originallyAssignedTrack = $channel.closest('.track-target');
      $unassignedInputsHolder.append($channel);
      $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length);

    }

    function addChannelToTrack($channel, $track) {
      var $originallyAssignedTrack = $channel.closest('.track-target');
      $track.append($channel);
      $track.attr('track-count', $track.find('.ftue-input:not(.ui-draggable-dragging)').length);
      $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length)
    }

    function addChannelToOutput($channel, $slot) {
      var $originallyAssignedTrack = $channel.closest('.output-target');
      $slot.append($channel);
      $slot.attr('output-count', $slot.find('.ftue-input:not(.ui-draggable-dragging)').length);
      $originallyAssignedTrack.attr('output-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length)
    }


    function initializeUnassignedOutputDroppable() {
      $unassignedOutputsHolder.droppable(
        {
          accept: '.ftue-input[data-direction="out"]',
          activeClass: 'drag-in-progress',
          hoverClass: 'drag-hovering',
          drop: function( event, ui ) {
            var $channel = ui.draggable;

            //$channel.css('left', '0').css('top', '0');
            unassignOutputChannel($channel);
          }
        });
    }

    function initializeUnassignedInputDroppable() {
      $unassignedInputsHolder.droppable(
        {
          accept: '.ftue-input[data-direction="in"]',
          activeClass: 'drag-in-progress',
          hoverClass: 'drag-hovering',
          drop: function( event, ui ) {
            var $channel = ui.draggable;
            //$channel.css('left', '0').css('top', '0');
            unassignInputChannel($channel);
          }
        });
    }

    function initializeOutputDroppables() {
      var i;
      for(i = 0; i < MAX_OUTPUTS; i++) {
        var $target = $(context._.template($templateOutputTarget.html(), {num: i }, { variable: 'data' }));
        $outputChannelHolder.append($target);
        $target.find('.output-target').droppable(
          {
            accept: '.ftue-input[data-direction="out"]',
            activeClass: 'drag-in-progress',
            hoverClass: 'drag-hovering',
            drop: function( event, ui ) {
              var $slot = $(this);
              if($slot.attr('output-count') == 1) {
                return false; // max of 1 output per slot
              }
              var $channel = ui.draggable;
              //$channel.css('left', '0').css('top', '0');
              addChannelToOutput($channel, $slot);
            }
          });
      }
    }

    function initializeTrackDroppables() {
      var i;
      for(i = 0; i < MAX_TRACKS; i++) {
        var $target = $(context._.template($templateTrackTarget.html(), {num: i }, { variable: 'data' }));
        $tracksHolder.append($target);
        $target.find('.track-target').droppable(
          {
            accept: '.ftue-input[data-direction="in"]',
            activeClass: 'drag-in-progress',
            hoverClass: 'drag-hovering',
            drop: function( event, ui ) {
              var $track = $(this);
              if($track.attr('track-count') == 2) {
                return false; // max of 2 inputs per track
              }

              var $channel = ui.draggable;
              //$channel.css('left', '0').css('top', '0');
              addChannelToTrack($channel, $track);
            }
          });
      }
    }

    function initializeInstrumentDropdown() {
      var i;
      for(i = 0; i < MAX_TRACKS; i++) {
        var $root = $('<div class="track-instrument"></div>');
        $root.instrumentSelector().attr('data-num', i);
        $instrumentsHolder.append($root);
      }
    }


    function initialize(_$parent) {
      $parent = _$parent;

      $templateAssignablePort = $('#template-assignable-port');
      $templateTrackTarget = $('#template-track-target');
      $templateOutputTarget = $('#template-output-target');
      $unassignedInputsHolder = $parent.find('.unassigned-input-channels')
      $unassignedOutputsHolder = $parent.find('.unassigned-output-channels');
      $tracksHolder = $parent.find('.tracks');
      $instrumentsHolder = $parent.find('.instruments');
      $outputChannelHolder = $parent.find('.output-channels');


      initializeUnassignedInputDroppable();
      initializeTrackDroppables();
      initializeInstrumentDropdown();

      initializeUnassignedOutputDroppable();
      initializeOutputDroppables();
    }

    this.initialize = initialize;
    this.trySave = trySave;
    this.reset = reset;

    return this;
  };

})(window, jQuery);