context = window

logger = context.JK.logger
ChannelGroupIds = context.JK.ChannelGroupIds
CategoryGroupIds = context.JK.CategoryGroupIds
MIX_MODES = context.JK.MIX_MODES;


@MixerHelper = class MixerHelper

  constructor: (@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @clientsWithAudioOverride, @mixMode) ->
    @mixMode = MIX_MODES.PERSONAL # TODO - remove mixMode from MixerHelper? Or at least stop using it in most functions
    @app = @session.app
    @mixersByResourceId = {}
    @mixersByTrackId = {}
    @allMixers = {}
    @currentMixerRangeMin = null
    @currentMixerRangeMax = null
    @mediaSummary = {}
    @mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup,
                         ChannelGroupIds.MetronomeGroup]
    @muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MidiInputMusicGroup, ChannelGroupIds.MediaTrackGroup,
                                        ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup]
    @vuStats = {}
    @shouldCollectVuStats = false
    @simulatedMusicCategoryMixers = {}
    @simulatedChatCategoryMixers = {}
    @organize()

  organize: () ->
    for masterMixer in @masterMixers
      @allMixers['M' + masterMixer.id] = masterMixer; # populate allMixers by mixer.id

      # populate mixer pair
      mixerPair = {}
      @mixersByResourceId[masterMixer.rid] = mixerPair
      @mixersByTrackId[masterMixer.id] = mixerPair
      mixerPair.master = masterMixer;

    for personalMixer in @personalMixers

      @allMixers['P' + personalMixer.id] = personalMixer

      # populate other side of mixer pair

      mixerPair = @mixersByResourceId[personalMixer.rid]
      unless mixerPair
        if personalMixer.group_id != ChannelGroupIds.MonitorGroup
          logger.warn("there is no master version of ", personalMixer)

        mixerPair = {}
        @mixersByResourceId[personalMixer.rid] = mixerPair

      @mixersByTrackId[personalMixer.id] = mixerPair;
      mixerPair.personal = personalMixer;

    @groupTypes()
    @chatMixer = @resolveChatMixer()

  groupTypes: () ->
    localMediaMixers = @mixersForGroupIds(@mediaTrackGroups, MIX_MODES.MASTER)
    peerLocalMediaMixers = @mixersForGroupId(ChannelGroupIds.PeerMediaTrackGroup, MIX_MODES.MASTER)

    #logger.debug("localMediaMixers", localMediaMixers)
    #logger.debug("peerLocalMediaMixers", peerLocalMediaMixers)

    # get the server data regarding various media tracks
    recordedBackingTracks = @session.recordedBackingTracks()
    backingTracks = @session.backingTracks()
    recordedJamTracks = @session.recordedJamTracks()
    jamTracks = @session.jamTracks()
    jamTrackMixdown = @session.jamTrackMixdown()

    ###
    with mixer info, we use these to decide what kind of tracks are open in the backend

    each mixer has a media_type field, which describes the type of media track it is.
    * JamTrack
    * BackingTrack
    * RecordingTrack
    * MetronomeTrack
    * "" - adhoc track (not supported visually)

    it is supposed to be the case that there are only one type of track open at a time, however, that's a business policy/logic
    constraint; and may be buggy. **So, we should render whatever we have, so that it's obvious what's really going on.**

    so, let's group up all mixers by type, and then ask them to be rendered
    ###

    @recordingTrackMixers = []
    @backingTrackMixers = []
    @jamTrackMixers = []
    @metronomeTrackMixers = []
    @adhocTrackMixers = []


    groupByType = (mixers, isLocalMixer) =>
      for mixer in mixers
        mediaType = mixer.media_type
        groupId = mixer.group_id

        if mediaType == 'MetronomeTrack' || groupId == ChannelGroupIds.MetronomeGroup
          # Metronomes come across with a blank media type, so check group_id:
          @metronomeTrackMixers.push(mixer)
        else if mediaType == null || mediaType == "" || mediaType == 'RecordingTrack'
          # additional check; if we can match an id in backing tracks or recorded backing track,
          # we need to remove it from the recorded track set, but move it to the backing track set

          isJamTrack = false;

          if mixer.id == jamTrackMixdown.id
            isJamTrack = true;

          if !isJamTrack && jamTracks
            # check if the ID matches that of an open jam track
            for jamTrack in jamTracks
              if mixer.id == jamTrack.id
                isJamTrack = true;
                break

          if !isJamTrack && recordedJamTracks
            # then check if the ID matches that of a open, recorded jam track
            for recordedJamTrack in recordedJamTracks
              if mixer.id == recordedJamTrack.id
                isJamTrack = true
                break

          if isJamTrack
            @jamTrackMixers.push(mixer)
          else
            isBackingTrack = false
            if recordedBackingTracks
              for recordedBackingTrack in recordedBackingTracks
                if mixer.id == 'L' + recordedBackingTrack.client_track_id
                  isBackingTrack = true
                  break

            if backingTracks
              for backingTrack in backingTracks
                if mixer.id == 'L' + backingTrack.client_track_id
                  isBackingTrack = true
                  break

            if isBackingTrack
              @backingTrackMixers.push(mixer)
            else
              # couldn't resolve this as a JamTrack or Backing track, must be a normal recorded file
              @recordingTrackMixers.push(mixer)

        else if mediaType == 'PeerMediaTrack' || mediaType == 'BackingTrack'
          @backingTrackMixers.push(mixer)
        else if mediaType == 'JamTrack'
          @jamTrackMixers.push(mixer);
        else if mediaType == null || mediaType == "" || mediaType == 'RecordingTrack'
          # mediaType == null is for backwards compat with older clients.  Can be removed soon
          @recordingTrackMixers.push(mixer)
        else
          logger.warn("Unknown track type: " + mediaType)
          @adhocTrackMixers.push(mixer)

    groupByType(localMediaMixers, true);
    groupByType(peerLocalMediaMixers, false);

    ###
    if recordingTrackMixers.length > 0
      renderRecordingTracks(recordingTrackMixers)

    if backingTrackMixers.length > 0
      renderBackingTracks(backingTrackMixers)

    if jamTrackMixers.length > 0
      renderJamTracks(jamTrackMixers);

    if metronomeTrackMixers.length > 0 && @session.jamTracks() == null && @session.recordedJamTracks() == null
      renderMetronomeTracks(metronomeTrackMixers);

    checkMetronomeTransition();
    ###

    @backingTracks = @resolveBackingTracks()
    @jamTracks = @resolveJamTracks()
    @recordedTracks = @resolveRecordedTracks()
    @metronome = @resolveMetronome()

    if @adhocTrackMixers.length > 0
      logger.warn("some tracks are open that we don't know how to show")

    @mediaSummary =
      recordingOpen: @recordedTracks.length > 0
      jamTrackOpen: @jamTracks.length > 0
      backingTrackOpen: @backingTracks.length > 0
      metronomeOpen: @session.isMetronomeOpen()



    # figure out if any media is open
    mediaOpenSummary = false
    for mediaType, mediaOpen of @mediaSummary
      mediaOpenSummary = true if mediaOpen

    @mediaSummary.mediaOpen = mediaOpenSummary

    # the user needs media controls if any media is open, or, if the user has indicated they want to open a JamTrack
    @mediaSummary.userNeedsMediaControls = @mediaSummary.mediaOpen || window.JamTrackStore.jamTrack?

    # this defines what the user wants to be open, not what actually is open in the backend and/or session
    @mediaSummary.jamTrack = window.JamTrackStore.jamTrack

    # figure out if we opened any media
    isOpener = false

    if @mediaSummary.recordingOpen
      isOpener = @recordedTracks[0].isOpener
    else if @mediaSummary.jamTrackOpen
      isOpener = @jamTracks[0].isOpener
    else if @mediaSummary.backingTrackOpen
      isOpener = @backingTracks[0].isOpener

    @mediaSummary.isOpener = isOpener

    @prepareSimulatedMixers()


  # this method is pretty complicated because it forks on a key bit of state:
  # sessionModel.isPlayingRecording()
  # a backing track opened as part of a recording has a different behavior and presence on the server (recording.recorded_backing_tracks)
  # than a backing track opend ad-hoc  (connection.backing_tracks)

  resolveBackingTracks: () ->
    backingTracks = []

    return backingTracks unless @backingTrackMixers.length > 0

    # find both client and server representation of the backing track
    serverBackingTracks = []
    backingTrackMixers = @backingTrackMixers

    if @session.isPlayingRecording()
      backingTrackMixers = context._.filter(backingTrackMixers, (mixer) -> return mixer.managed || !mixer.managed?)
      serverBackingTracks = @session.recordedBackingTracks()
    else
      serverBackingTracks = @session.backingTracks();
      backingTrackMixers = context._.filter(backingTrackMixers, (mixer) -> return !mixer.managed)
      if backingTrackMixers.length > 1
        logger.error("multiple, managed backing track mixers encountered", backingTrackMixers)
        @app.notify({
          title: "Multiple Backing Tracks Encountered",
          text: "Only one backing track can be open a time.",
          icon_url: "/assets/content/icon_alert_big.png"
        });
        return backingTracks;

    # we don't render backing tracks unless we have server data to accompany
    if !serverBackingTracks? || serverBackingTracks.length == 0
      return backingTracks

    noCorrespondingTracks = false
    for mixer in backingTrackMixers
      # find the track or tracks that correspond to the mixer
      correspondingTracks = []
      noCorrespondingTracks = false
      if @session.isPlayingRecording()
        for backingTrack in serverBackingTracks
          # occurs if this client is the one that opened the track, # occurs if this client is a remote participant
          if mixer.persisted_track_id == backingTrack.client_track_id || mixer.id == 'L' + backingTrack.client_track_id
            correspondingTracks.push(backingTrack)
      else
        # if this is just an open backing track, then we can assume that the 1st backingTrackMixer is ours
        correspondingTracks.push(serverBackingTracks[0])

      if correspondingTracks.length == 0
        noCorrespondingTracks = true
        logger.debug("renderBackingTracks: could not map backing tracks")
        @app.notify({
          title: "Unable to Open Backing Track",
          text: "Could not correlate server and client tracks",
          icon_url: "/assets/content/icon_alert_big.png"
        });
        break

      # now we have backing track and mixer in hand; we can render
      serverBackingTrack = correspondingTracks[0]

      oppositeMixer  = @getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL);

      isOpener = mixer.group_id == ChannelGroupIds.MediaTrackGroup
      data =
        isOpener:  isOpener
        shortFilename: context.JK.getNameOfFile(serverBackingTrack.filename)
        instrumentIcon: context.JK.getInstrumentIcon45(serverBackingTrack.instrument_id)
        photoUrl: "/assets/content/icon_recording.png"
        showLoop: isOpener && !@session.isPlayingRecording()
        track: serverBackingTrack
        mixers:  @mediaMixers(mixer, isOpener)

      backingTracks.push(data)

    backingTracks

  resolveJamTracks: () ->
    _jamTracks = []

    return _jamTracks unless @jamTrackMixers.length > 0


    jamTrackMixers = @jamTrackMixers.slice();
    jamTracks = []
    jamTrackName = null;
    jamTrackMixdown = {id: null}

    if @session.isPlayingRecording()
      # only return managed mixers for recorded backing tracks
      jamTracks = @session.recordedJamTracks()
      jamTrackName = @session.recordedJamTrackName()
    else
      # only return un-managed (ad-hoc) mixers for normal backing tracks
      jamTracks = @session.jamTracks()
      jamTrackName = @session.jamTrackName()
      jamTrackMixdown = @session.jamTrackMixdown()

    # pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer)
    # if it's a locally opened track (JamTrackGroup), then we can say this person is the opener
    isOpener = jamTrackMixers[0].group_id == ChannelGroupIds.JamTrackGroup;

    if jamTracks
      noCorrespondingTracks = false

      # Are we opening a mixdown, or a full track?
      if jamTrackMixdown.id?
        logger.debug("MixerHelper: mixdown is active. id: #{jamTrackMixdown.id}")
        if jamTrackMixers.length == 0
          noCorrespondingTracks = true
          logger.error("could not correlate mixdown tracks", jamTrackMixers, jamTrackMixdown)
          @app.notify({
            title: "Unable to Open Custom Mix",
            text: "Could not correlate server and client tracks",
            icon_url: "/assets/content/icon_alert_big.png"})
          return _jamTracks
        else if jamTrackMixers.length > 1
          logger.warn("ignoring wrong amount of mixers for JamTrack in mixdown mode")
          return _jamTracks
        else

          instrumentIcon = context.JK.getInstrumentIcon24('other')
          part = null
          instrumentName = 'Custom Mix'
          trackName = 'Custom Mix'

          data =
            name: jamTrackName
            trackName: trackName
            part: part
            isOpener:  isOpener
            instrumentIcon: instrumentIcon
            track: jamTrackMixdown
            mixers: @mediaMixers(jamTrackMixers[0], isOpener)

          _jamTracks.push(data)
      else
        logger.debug("MixerHelper: full jamtrack is active")

        if jamTrackMixers.length == 1
          logger.warn("ignoring wrong amount of mixers for JamTrack in Full Track mode")
          return _jamTracks

        for jamTrack in jamTracks
          mixer = null
          preMasteredClass = ""
          # find the track or tracks that correspond to the mixer
          correspondingTracks = []

          for matchMixer in @jamTrackMixers
            if matchMixer.id == jamTrack.id
              correspondingTracks.push(jamTrack)
              mixer = matchMixer

          if correspondingTracks.length == 0
            noCorrespondingTracks = true
            logger.error("could not correlate jam tracks", jamTrackMixers, jamTracks)
            @app.notify({
              title: "Unable to Open JamTrack",
              text: "Could not correlate server and client tracks",
              icon_url: "/assets/content/icon_alert_big.png"})
            return _jamTracks

          #jamTracks = $.grep(jamTracks, (value) =>
          #  $.inArray(value, correspondingTracks) < 0
          #)

          # prune found mixers
          jamTrackMixers.splice(mixer);

          oneOfTheTracks = correspondingTracks[0];
          instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument.id);

          part = oneOfTheTracks.part

          instrumentName = oneOfTheTracks.instrument.description

          if part?
            trackName = "#{instrumentName}: #{part}"
          else
            trackName = instrumentName

          if jamTrack.track_type == 'Click'
            trackName = 'Clicktrack'

          data =
            name: jamTrackName
            trackName: trackName
            part: part
            isOpener:  isOpener
            instrumentIcon: instrumentIcon
            track: oneOfTheTracks
            mixers: @mediaMixers(mixer, isOpener)

          _jamTracks.push(data)

    _jamTracks

  resolveRecordedTracks: () ->
    recordedTracks = []

    return recordedTracks unless @recordingTrackMixers.length > 0

    serverRecordedTracks = @session.recordedTracks()

    isOpener = @recordingTrackMixers[0].group_id == ChannelGroupIds.MediaTrackGroup

    # using the server's info in conjuction with the client's, draw the recording tracks
    if serverRecordedTracks
      recordingName = @session.recordingName()
      noCorrespondingTracks = false
      for mixer in @recordingTrackMixers
        preMasteredClass = ""
        correspondingTracks = []
        for recordedTrack in serverRecordedTracks
          if mixer.id.indexOf("L") == 0
            if mixer.id.substring(1) == recordedTrack.client_track_id
              correspondingTracks.push(recordedTrack)
          else if mixer.id.indexOf("C") == 0
            if mixer.id.substring(1) == recordedTrack.client_id
              correspondingTracks.push(recordedTrack)
              preMasteredClass = "pre-mastered-track"
          else
            # this should not be possible
            alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id")

        if correspondingTracks.length == 0
          noCorrespondingTracks = true
          logger.debug("unable to correlate all recorded tracks", recordingMixers, serverRecordedTracks)
          @app.notify({
            title: "Unable to Open Recording",
            text: "Could not correlate server and client tracks",
            icon_url: "/assets/content/icon_alert_big.png"});
          return recordedTracks

        serverRecordedTracks = $.grep(serverRecordedTracks,
        (value) =>
          $.inArray(value, correspondingTracks) < 0
        )

        oneOfTheTracks = correspondingTracks[0]
        instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument_id)
        userName = oneOfTheTracks.user.name
        userName = oneOfTheTracks.user.first_name + ' ' + oneOfTheTracks.user.last_name unless userName?

        data =
          recordingName: recordingName
          isOpener:  isOpener
          userName: userName
          instrumentIcon: instrumentIcon
          track: oneOfTheTracks
          mixers:  @mediaMixers(mixer, isOpener)

        recordedTracks.push(data)

    recordedTracks

  resolveMetronome: () ->

    return null if @metronomeTrackMixers.length == 0

    mixer = @metronomeTrackMixers[0]

    instrumentIcon = "/assets/content/icon_metronome.png"

    metronome =
      instrumentIcon: instrumentIcon
      mixers:  @mediaMixers(mixer, true)

    metronome

  resolveChatMixer: () ->
    masterChatMixers = @mixersForGroupId(ChannelGroupIds.AudioInputChatGroup, MIX_MODES.MASTER);

    return null if masterChatMixers.length == 0

    personalChatMixers = @mixersForGroupId(ChannelGroupIds.AudioInputChatGroup, MIX_MODES.PERSONAL);

    if personalChatMixers.length == 0
      logger.warn("unable to find personal mixer for voice chat");
      return null


    masterChatMixer = masterChatMixers[0];
    personalChatMixer  = personalChatMixers[0];

    {
      master: {
        mixer: masterChatMixer
        muteMixer: masterChatMixer
        vuMixer: masterChatMixer
        oppositeMixer: personalChatMixer
      }
      personal: {
        mixer: personalChatMixer
        muteMixer: personalChatMixer
        vuMixer: personalChatMixer
        oppositeMixer: masterChatMixer
      }
    }

  # supply the master mixer of a media track, and this function will harvest out the rest
  mediaMixers:(masterMixer, isOpener) ->
    personalMixer = if isOpener then @getMixerByResourceId(masterMixer.rid, MIX_MODES.PERSONAL) else masterMixer
    personalVuMixer = if isOpener then personalMixer else masterMixer
    {
      isOpener: isOpener

      master: {
        mixer: masterMixer
        muteMixer: masterMixer
        vuMixer: masterMixer
      }
      personal: {
        mixer: personalMixer
        muteMixer: personalMixer
        vuMixer: personalVuMixer
      }
    }


  mixersForGroupIds: (groupIds, mixMode) ->
    foundMixers = []
    mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers;

    for mixer in mixers
      for groupId in groupIds
        if mixer.group_id == groupId
          foundMixers.push(mixer)

    foundMixers

  mixersForGroupId: (groupId, mixMode) ->
    foundMixers = [];
    mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers;
    for mixer in mixers
      if mixer.group_id == groupId
        foundMixers.push(mixer)

    foundMixers

  mixerForGroupId: (groupId, mixMode) ->
    mixers = @mixersForGroupId(groupId, mixMode)
    if mixers? && mixers.length > 0
      mixers[0]
    else
      null

  getMixer: (mixerId, mode) ->
    beforeMode = mode
    mode = @mixMode unless mode?
    @allMixers[(if mode then 'M' else 'P') + mixerId]

  getMixerByTrackId: (trackId, mode)  ->
    mixerPair = @mixersByTrackId[trackId]

    return null unless mixerPair

    if mode == undefined
      return mixerPair

    else
      if mode == MIX_MODES.MASTER
        return mixerPair.master
      else
        return mixerPair.personal


  groupedMixersForClientId: (clientId, groupIds, usedMixers, mixMode) ->
    foundMixers = {};
    mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers;

    for mixer in mixers
      unless mixer?
        #logger.debug("empty mixer: ", mixers)
        continue

      if mixer.client_id == clientId
        for groupId in groupIds
          if mixer.group_id == groupId
            if (mixer.groupId != ChannelGroupIds.UserMusicInputGroup) && !(mixer.id of usedMixers)
              mixers = foundMixers[mixer.group_id]
              if !mixers
                mixers = []
                foundMixers[mixer.group_id] = mixers
              mixers.push(mixer)

    foundMixers

  getMixerByResourceId:(resourceId, mode) ->
    mixerPair = @mixersByResourceId[resourceId];

    return null if(!mixerPair)

    if !mode?
      return mixerPair;
    else
      if mode == MIX_MODES.MASTER
        return mixerPair.master
      else
        return mixerPair.personal


  findMixerForTrack: (client_id, track, myTrack, mode) ->
    mixer = null # what is the best mixer for this track/client ID?
    oppositeMixer = null # what is the corresponding mixer in the opposite mode?
    vuMixer = null
    muteMixer = null

    if myTrack
      # when it's your track, look it up by the backend resource ID
      mixer = @getMixerByTrackId(track.client_track_id, mode)
      vuMixer = mixer
      muteMixer = mixer

      # sanity checks
      if mixer && (mixer.group_id != ChannelGroupIds.AudioInputMusicGroup && mixer.group_id != ChannelGroupIds.MidiInputMusicGroup)
        logger.error("found local mixer that was not of groupID: AudioInputMusicGroup", mixer)

      if mixer
        # find the matching AudioInputMusicGroup for the opposite mode
        oppositeMixer = @getMixerByTrackId(track.client_track_id, !mode)

        if mode == MIX_MODES.PERSONAL
          muteMixer = oppositeMixer; # make the master mixer the mute mixer

        # sanity checks
        if !oppositeMixer
          logger.error("unable to find opposite mixer for local mixer", mixer)
        else if oppositeMixer.group_id != ChannelGroupIds.AudioInputMusicGroup && oppositeMixer.group_id != ChannelGroupIds.MidiInputMusicGroup
          logger.error("found local mixer in opposite mode that was not of groupID: AudioInputMusicGroup", mixer, oppositeMixer)
      else
        logger.debug("local track is not present: ", track, @allMixers)
    else
      switch mode
        when MIX_MODES.MASTER

          # when it's a remote track and in master mode, we should find the PeerAudioInputMusicGroup
          mixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER)

          # sanity check
          if mixer && (mixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup && mixer.group_id != ChannelGroupIds.PeerMidiInputMusicGroup)
            logger.warn("master: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer)

          vuMixer = mixer
          muteMixer = mixer

          if mixer
            # we should be able to find a UserMusicInputGroup for this clientId in personal mode
            oppositeMixers  = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL)
            if oppositeMixers[ChannelGroupIds.UserMusicInputGroup]
              oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0]

            if !oppositeMixer
              logger.warn("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer, @personalMixers)

        when MIX_MODES.PERSONAL
          mixers  = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL)
          if mixers[ChannelGroupIds.UserMusicInputGroup]
            mixer = mixers[ChannelGroupIds.UserMusicInputGroup][0]

            vuMixer = mixer
            muteMixer = mixer

            if mixer
              # now grab the PeerAudioInputMusicGroup in master mode to satisfy the 'opposite' mixer
              oppositeMixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER)
              if !oppositeMixer
                logger.debug("personal: unable to find a PeerAudioInputMusicGroup master mixer matching a UserMusicInput", client_id, track.client_track_id)
              else if oppositeMixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup && oppositeMixer.group_id != mixer.group_id != ChannelGroupIds.PeerMidiInputMusicGroup
                logger.error("personaol: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer)

              #vuMixer = oppositeMixer; # for personal mode, use the PeerAudioInputMusicGroup's VUs
          else
            logger.error("no UserMusicInputGroup for client_id #{client_id} in PERSONAL mode", mixers)

    {
      mixer: mixer,
      oppositeMixer: oppositeMixer,
      vuMixer: vuMixer,
      muteMixer: muteMixer
    }

  mute: (mixerId, mode, muting) ->

    mode = @mixMode unless mode?

    @fillTrackVolumeObject(mixerId, mode)

    context.trackVolumeObject.mute = muting

    context.jamClient.SessionSetControlState(mixerId, mode)

    # keep state of mixer in sync with backend
    mixer =  @getMixer(mixerId, mode)
    mixer.mute = muting

  getOriginalVolume: (mixers, gainType) ->
    originalVolume = null
    if gainType == 'music'

      # find a non media volL to compare against for later 'relative move'
      for mixer in mixers
        if mixer.name != CategoryGroupIds.UserMedia && mixer.name != CategoryGroupIds.MediaTrack
          originalVolume = mixer.volume_left
          break
    else
      originalVolume = mixers[0].volume_left

    originalVolume

  faderChanged: (data, mixers, gainType, controlGroup) ->
    mixers = [mixers] unless $.isArray(mixers)

    originalVolume = @getOriginalVolume(mixers, gainType)

    if controlGroup?
      mixers = [mixers[0]]

      for mixer in mixers
        broadcast = !(data.dragging)  # If fader is still dragging, don't broadcast
        mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast)

        relative = gainType == 'music' && (mixer.name == CategoryGroupIds.UserMedia || mixer.name == CategoryGroupIds.MediaTrack)

        @setMixerVolume(mixer, data.percentage, relative, originalVolume, controlGroup)

        # keep state of mixer in sync with backend
        mixer =  @getMixer(mixer.id, mixer.mode)
        mixer.volume_left = context.trackVolumeObject.volL

    else

      for mixer in mixers
        broadcast = !(data.dragging)  # If fader is still dragging, don't broadcast
        mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast)

        relative = gainType == 'music' && (mixer.name == CategoryGroupIds.UserMedia || mixer.name == CategoryGroupIds.MediaTrack)

        @setMixerVolume(mixer, data.percentage, relative, originalVolume)

        # keep state of mixer in sync with backend
        mixer =  @getMixer(mixer.id, mixer.mode)
        mixer.volume_left = context.trackVolumeObject.volL

        #if groupId == ChannelGroupIds.UserMusicInputGroup
        #  # there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well
        #  context.JK.FaderHelpers.setFaderValue(mixerId, data.percentage)

  initGain: (mixer) ->
    if $.isArray(mixer)
      mixer = mixer[0]

    gainPercent = context.JK.FaderHelpers.convertAudioTaperToPercent(mixer.volume_left)
    context.JK.FaderHelpers.setFaderValue(mixer.id, gainPercent)
    context.JK.FaderHelpers.showFader(mixer.id)

  panChanged: (data, mixers, groupId) ->
    mixers = [mixers] unless $.isArray(mixers)
      # media tracks are the only controls that sometimes set two mixers right now
    for mixer in mixers
      broadcast = !(data.dragging)  # If fader is still dragging, don't broadcast
      mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast)

      @setMixerPan(mixer, data.percentage)

      # keep state of mixer in sync with backend
      mixer =  @getMixer(mixer.id, mixer.mode)
      mixer.pan = context.trackVolumeObject.pan

  initPan: (mixer) ->
    panPercent= context.JK.PanHelpers.convertPanToPercent(mixer.pan)
    context.JK.FaderHelpers.setFaderValue(mixer.id, panPercent, Math.abs(mixer.pan))
    context.JK.FaderHelpers.showFader(mixer.id)

  setMixerPan: (mixer, panPercent) ->

    context.trackVolumeObject.pan = context.JK.PanHelpers.convertPercentToPan(panPercent);
    context.jamClient.SessionSetControlState(mixer.id, mixer.mode);

  loopChanged: (mixer, shouldLoop) ->

    @fillTrackVolumeObject(mixer.id, mixer.mode)
    context.trackVolumeObject.loop = shouldLoop
    context.jamClient.SessionSetControlState(mixer.id, mixer.mode)

    # keep state of mixer in sync with backend
    mixer =  @getMixer(mixer.id, mixer.mode)
    mixer.loop = context.trackVolumeObject.loop

  setMixerVolume: (mixer, volumePercent, relative, originalVolume, controlGroup) ->
    ###
    // The context.trackVolumeObject has been filled with the mixer values
    // that go with mixerId, and the range of that mixer
    // has been set in currentMixerRangeMin-Max.
    // All that needs doing is to translate the incoming percent
    // into the real value ont the sliders range. Set Left/Right
    // volumes on trackVolumeObject, and call SetControlState to stick.
    ###

    newVolume = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent);
    if relative
      context.trackVolumeObject.volL = context.trackVolumeObject.volL + (newVolume - originalVolume)
      context.trackVolumeObject.volR = context.trackVolumeObject.volR + (newVolume - originalVolume)

      # keep within range
      if context.trackVolumeObject.volL < -80
        context.trackVolumeObject.volL = -80
      else if context.trackVolumeObject.volL > 20
        context.trackVolumeObject.volL = 20

      if context.trackVolumeObject.volR < -80
        context.trackVolumeObject.volR = -80
      else if context.trackVolumeObject.volR > 20
        context.trackVolumeObject.volR = 20


    else
      context.trackVolumeObject.volL = newVolume
      context.trackVolumeObject.volR = newVolume
    if controlGroup?

      if mixer.mode == MIX_MODES.PERSONAL
        controlGroupsArg = 0
      else
        controlGroupsArg = 1
      context.jamClient.setSessionMixerCategoryPlayoutState(controlGroup == 'music', controlGroupsArg);
    else
      context.jamClient.SessionSetControlState(mixer.id, mixer.mode);

  percentFromMixerValue: (min, max, value) ->
    try
      range = Math.abs(max - min)
      magnitude = value - min
      percent = Math.round(100*(magnitude/range))
      percent
    catch err
      0


  percentToMixerValue:(min, max, percent) ->
    range = Math.abs(max - min);
    multiplier = percent/100; # Change 85 into 0.85
    value = min + (multiplier * range);

    # Protect against percents < 0 and > 100
    if value < min
      value = min;

    if value > max
      value = max;

    return value;

  fillTrackVolumeObject: (mixerId, mode, broadcast) ->
    _broadcast = true
    if broadcast?
      _broadcast = broadcast

    mixer =  @getMixer(mixerId, mode)
    context.trackVolumeObject.clientID = mixer.client_id
    context.trackVolumeObject.broadcast = _broadcast
    context.trackVolumeObject.master = mixer.master
    context.trackVolumeObject.monitor = mixer.monitor
    context.trackVolumeObject.mute = mixer.mute
    context.trackVolumeObject.name = mixer.name
    context.trackVolumeObject.record = mixer.record
    context.trackVolumeObject.volL = mixer.volume_left
    context.trackVolumeObject.pan = mixer.pan

    # today we treat all tracks as mono, but this is required to make a stereo track happy
    # context.trackVolumeObject.volR = mixer.volume_right;
    context.trackVolumeObject.volR = mixer.volume_left;

    context.trackVolumeObject.loop = mixer.loop;
    # trackVolumeObject doesn't have a place for range min/max
    @currentMixerRangeMin = mixer.range_low;
    @currentMixerRangeMax = mixer.range_high;
    mixer

  collectStats: (mixer) ->
    mixerStats = @vuStats[mixer.id]

    unless mixerStats?
      mixerStats = {count: 0, group_name: context.JK.groupIdDisplay(mixer)}
      @vuStats[mixer.id] = mixerStats

    mixerStats.count++

  dumpVUStats: () ->

    # to use: check MixerStore for setInterval in cstr
    logger.debug("VU STAT DUMP")
    for mixerId, mixerStat of @vuStats
      logger.debug("VU STAT: #{mixerState.group_name} count=#{mixerStat.count}")

  updateVU: (mixerId, mode, leftValue, leftClipping, rightValue, rightClipping) ->
    mixer = @getMixer(mixerId, mode)

    if mixer?
      @collectStats(mixer) if @shouldCollectVuStats
      context.JK.VuHelpers.updateVU3(mixer, leftValue, leftClipping, rightValue, rightClipping)

    ###
    if mixer
      if mixer.stereo # // stereo track
        if mixerId.substr(-4) == "_vul"
          context.JK.VuHelpers.updateVU2('vul', mixer, value)
        else
          context.JK.VuHelpers.updateVU2('vur', mixer, value)
      else
        if mixerId.substr(-4) == "_vul"
          # Do the left
          context.JK.VuHelpers.updateVU2('vul', mixer, value)
          # Do the right
          context.JK.VuHelpers.updateVU2('vur', mixer, value)
    ###
  getTrackInfo: () ->
    context.JK.TrackHelpers.getTrackInfo(context.jamClient, @masterMixers)

  getGroupMixer: (categoryId, mode) ->
    groupId = if mode == MIX_MODES.MASTER then ChannelGroupIds.MasterCatGroup else ChannelGroupIds.MonitorCatGroup
    oppositeGroupId = if !mode == MIX_MODES.MASTER then ChannelGroupIds.MasterCatGroup else ChannelGroupIds.MonitorCatGroup
    mixers = @mixersForGroupId(groupId, mode)
    oppositeMixers = @mixersForGroupId(oppositeGroupId, !mode)

    if mixers.length == 0
      #logger.warn("could not find mixer with group ID: " + groupId + ', mode:' + mode)
      return null

    found = null
    oppositeFound = null
    for mixer in mixers
      if mixer.name == categoryId
        found = mixer
        break

    for mixer in oppositeMixers
      if mixer.name == categoryId
        oppositeFound = mixer
        break

    unless found?
      logger.warn("could not find mixer with categoryId: " + categoryId)
      return null
    else
      {
      mixer: found,
      muteMixer : found,
      vuMixer: found,
      oppositeMixer: oppositeFound
      }

  prepareSimulatedMixers: () ->
    @simulatedMusicCategoryMixers[MIX_MODES.MASTER] = @getSimulatedMusicCategoryMixer(MIX_MODES.MASTER)
    @simulatedMusicCategoryMixers[MIX_MODES.PERSONAL] = @getSimulatedMusicCategoryMixer(MIX_MODES.PERSONAL)
    @simulatedChatCategoryMixers[MIX_MODES.MASTER] = @getSimulatedChatCategoryMixer(MIX_MODES.MASTER)
    @simulatedChatCategoryMixers[MIX_MODES.PERSONAL] = @getSimulatedChatCategoryMixer(MIX_MODES.PERSONAL)

  getSimulatedMusicCategoryMixer: (mode) ->
    myInputs = @getAudioInputCategoryMixer(mode)?.mixer
    peerInputs = @getUserMusicCategoryMixer(mode)?.mixer
    myMedia= @getMediaCategoryMixer(mode)?.mixer
    peerMedia = @getUserMediaCategoryMixer(mode)?.mixer
    metronome = @getMetronomeCategoryMixer(mode)?.mixer
    output = @getOutputMixer(mode)
    oppositeOutput = @getOutputMixer(!mode)

    # if myInputs category is missing, all categories are missing (seen when audio is first starting)
    if myInputs
      {
      first: myInputs
      mixer: [myInputs, peerInputs, myMedia, peerMedia, metronome]
      muteMixer: [myInputs, peerInputs, myMedia, peerMedia, metronome]
      vuMixer: output
      }
    else
      null


  getSimulatedChatCategoryMixer: (mode) ->
    myChats = @getChatCategoryMixer(mode)?.mixer
    peerChats = @getUserChatCategoryMixer(mode)?.mixer
    if myChats
      {
        first: myChats
        mixer: [myChats, peerChats]
        muteMixer: [myChats, peerChats]
        vuMixer: myChats
      }
    else
      null


  getAudioInputCategoryMixer: (mode) ->
    @getGroupMixer(CategoryGroupIds.AudioInputMusic, mode)

  getChatCategoryMixer: (mode) ->
    @getGroupMixer(CategoryGroupIds.AudioInputChat, mode)

  getUserChatCategoryMixer: (mode) ->
    @getGroupMixer(CategoryGroupIds.UserChat, mode)

  getMediaCategoryMixer: (mode) ->
    @getGroupMixer(CategoryGroupIds.MediaTrack, mode)

  getUserMediaCategoryMixer: (mode) ->
    @getGroupMixer(CategoryGroupIds.UserMedia, mode)

  getUserMusicCategoryMixer: (mode) ->
    @getGroupMixer(CategoryGroupIds.UserMusic, mode)

  getMetronomeCategoryMixer: (mode) ->
    @getGroupMixer(CategoryGroupIds.Metronome, mode)

  getOutputCategoryMixer: (mode) ->
    if mode == MIX_MODES.MASTER
      @getGroupMixer(CategoryGroupIds.MasterCatGroup, mode)
    else
      @getGroupMixer(CategoryGroupIds.MonitorCatGroup, mode)

  getOutputMixer: (mode) ->
    if mode == MIX_MODES.MASTER
      @mixerForGroupId(ChannelGroupIds.MasterGroup, mode)
    else
      @mixerForGroupId(ChannelGroupIds.MonitorGroup, mode)

  refreshMixer: (mixers) ->
    return null unless mixers? && mixers.mixer?

    updateMixers = null
    if $.isArray(mixers.mixer)
      if mixers.mixer.length > 0
        updateMixers = []
        for mixer in mixers.mixer
          updateMixers.push(@getMixer(mixer.id, mixer.mode))
    else
      updateMixers = @getMixer(mixers.mixer.id, mixers.mixer.mode)

    updatedVUMixers = null
    if $.isArray(mixers.vuMixer)
      updatedVUMixers = []
      for vuMixer in mixers.vuMixer
        updateVUMixers.push(@getMixer(vuMixer.id, vuMixer.mode))
    else
      updateVUMixers = @getMixer(mixers.vuMixer.id, mixers.vuMixer.mode)

    updateMuteMixers = null
    if $.isArray(mixers.muteMixer)
      updateMuteMixers = []
      for muteMixer in mixers.muteMixer
        updateMuteMixers.push(@getMixer(muteMixer.id, muteMixer.mode))
    else
      updateMuteMixers = @getMixer(mixers.muteMixer.id, mixers.muteMixer.mode)

    oppositeMixer = if mixers.oppositeMixer then @getMixer(mixers.oppositeMixer.id, mixers.oppositeMixer.mode) else null

    if updateMixers
      {
      mixer: updateMixers
      vuMixer: updateVUMixers
      muteMixer: updateMuteMixers
      oppositeMixer: oppositeMixer
      }
    else
      return null


  recordingName: () ->
    @session.recordingName()

  jamTrackName: () ->
    @session.jamTrackName()
