import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { ChannelGroupIds, CategoryGroupIds, MIX_MODES } from '../helpers/globals.js';

const context = window;
const logger = console; // for now

const useMixerHelper = () => {
  // Core state that was initialized in constructor
  const [mixersByResourceId, setMixersByResourceId] = useState({});
  const [mixersByTrackId, setMixersByTrackId] = useState({});
  const [allMixers, setAllMixers] = useState({});
  const [currentMixerRangeMin, setCurrentMixerRangeMin] = useState(null);
  const [currentMixerRangeMax, setCurrentMixerRangeMax] = useState(null);
  const [mediaSummary, setMediaSummary] = useState({});
  const [vuStats, setVuStats] = useState({});
  const [shouldCollectVuStats, setShouldCollectVuStats] = useState(false);
  const [simulatedMusicCategoryMixers, setSimulatedMusicCategoryMixers] = useState({});
  const [simulatedChatCategoryMixers, setSimulatedChatCategoryMixers] = useState({});
  const [vuMeterUpdatePrefMap, setVuMeterUpdatePrefMap] = useState({});

  // Constants (moved from constructor)
  const mediaTrackGroups = useMemo(() => [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup,
  ChannelGroupIds.MetronomeGroup], []);
  const muteBothMasterAndPersonalGroups = useMemo(() => [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MidiInputMusicGroup, ChannelGroupIds.MediaTrackGroup,
  ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup], []);

  // Track mixers state
  const [recordingTrackMixers, setRecordingTrackMixers] = useState([]);
  const [backingTrackMixers, setBackingTrackMixers] = useState([]);
  const [jamTrackMixers, setJamTrackMixers] = useState([]);
  const [metronomeTrackMixers, setMetronomeTrackMixers] = useState([]);
  const [adhocTrackMixers, setAdhocTrackMixers] = useState([]);

  // Resolved tracks state
  const [backingTracks, setBackingTracks] = useState([]);
  const [jamTracks, setJamTracks] = useState([]);
  const [recordedTracks, setRecordedTracks] = useState([]);
  const [metronome, setMetronome] = useState(null);
  const [chatMixer, setChatMixer] = useState(null);

  // Input parameters (replacing constructor params)
  const [session, setSession] = useState(null);
  const [masterMixers, setMasterMixers] = useState([]);
  const [personalMixers, setPersonalMixers] = useState([]);
  const [metro, setMetro] = useState(null);
  const [noAudioUsers, setNoAudioUsers] = useState({});
  const [clientsWithAudioOverride, setClientsWithAudioOverride] = useState({});
  const [mixMode, setMixMode] = useState(MIX_MODES.PERSONAL);

  // Initialization flag
  const isInitialized = useRef(false);
  const allMixersLoaded = useRef(false);


  // Initialization function that replaces the class constructor
  const initialize = useCallback((session, masterMixers, personalMixers, metro, noAudioUsers, clientsWithAudioOverride, mixMode) => {
    if (isInitialized.current) {
      logger.debug("MixerHelper: already initialized, skipping");
      return;
    }

    logger.debug("MixerHelper: initializing with constructor params", {
      session, masterMixers, personalMixers, metro, noAudioUsers, clientsWithAudioOverride, mixMode
    });

    // Set initial state from constructor parameters
    setSession(session);
    setMasterMixers(masterMixers || []);
    setPersonalMixers(personalMixers || []);
    setMetro(metro);
    setNoAudioUsers(noAudioUsers || {});
    setClientsWithAudioOverride(clientsWithAudioOverride || {});
    setMixMode(mixMode || MIX_MODES.PERSONAL);

    // Mark as initialized
    isInitialized.current = true;
    // Call organize to set up the mixer structure
    setTimeout(() => organize(), 0); // Defer to next tick to ensure state is set
  }, []);

  const organize = useCallback(() => {
    if (!session || masterMixers.length === 0 || personalMixers.length === 0) return;
    const newAllMixers = {};
    const newMixersByResourceId = {};
    const newMixersByTrackId = {};

    for (const masterMixer of masterMixers) {
      newAllMixers['M' + masterMixer.id] = masterMixer;

      const mixerPair = {};
      newMixersByResourceId[masterMixer.rid] = mixerPair;
      newMixersByTrackId[masterMixer.id] = mixerPair;
      mixerPair.master = masterMixer;
    }

    for (const personalMixer of personalMixers) {
      newAllMixers['P' + personalMixer.id] = personalMixer;

      let mixerPair = newMixersByResourceId[personalMixer.rid];
      if (!mixerPair) {
        if (personalMixer.group_id !== ChannelGroupIds.MonitorGroup) {
          logger.warn("there is no master version of ", personalMixer);
        }
        mixerPair = {};
        newMixersByResourceId[personalMixer.rid] = mixerPair;
      }
      newMixersByTrackId[personalMixer.id] = mixerPair;
      mixerPair.personal = personalMixer;
    }

    console.debug("MixerHelper: organize", { session, newAllMixers })
    setAllMixers(newAllMixers);
    setMixersByResourceId(newMixersByResourceId);
    setMixersByTrackId(newMixersByTrackId);

    const newChatMixer = resolveChatMixer(newMixersByResourceId, newAllMixers);
    setChatMixer(newChatMixer);

    groupTypes(newAllMixers);

  }, [session, masterMixers, personalMixers]);

  useEffect(() => {
    //allMixers not empty 
    if (allMixers && Object.keys(allMixers).length > 0) {
      allMixersLoaded.current = true;
      console.log("_TEST", Object.keys(allMixers))
    }
  }, [allMixers])

  useEffect(() => {
    organize();
  }, [session, masterMixers, personalMixers]);

  const update = useCallback((session, masterMixers, personalMixers, mixMode) => {
    logger.debug("MixerHelper: update called", { session, masterMixers, personalMixers, mixMode });
    setSession(session);
    setMasterMixers(masterMixers);
    setPersonalMixers(personalMixers);
    setMixMode(mixMode);
  }, []);

  // const getMixer = useCallback((mixerId, mode) => {
  //   if (!allMixersLoaded.current) return null;
  //   mode = mode || mixMode;
  //   console.debug("MixerHelper: getMixer", mixerId, mode, allMixers)
  //   return allMixers[(mode ? 'M' : 'P') + mixerId];
  // }, [allMixers, mixMode]);

  const getMixer = (mixerId, mode) => {
    if (!allMixersLoaded.current) return null;
    mode = mode || mixMode;
    console.debug("MixerHelper: getMixer", allMixersLoaded.current, mixerId, mode, allMixers)
    return allMixers[(mode ? 'M' : 'P') + mixerId];
  }

  const fillTrackVolumeObject = useCallback((mixerId, mode, currentAllMixers, broadcast = true) => {
    const mixer = getMixer(mixerId, mode, currentAllMixers);
    if (mixer == null) {
      console.error("MixerHelper: fillTrackVolumeObject: unable to find mixer with ID: #{mixerId}, mode: #{mode}");
      return null;
    }

    context.trackVolumeObject.id = mixer.id;
    context.trackVolumeObject._id = mixer._id;
    context.trackVolumeObject.groupID = mixer.group_id;
    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;
    context.trackVolumeObject.mediaType = mixer.media_type;
    context.trackVolumeObject.isJamTrack = mixer.is_jam_track;
    context.trackVolumeObject.isMetronome = mixer.is_metronome;

    context.trackVolumeObject.volR = mixer.volume_left;

    context.trackVolumeObject.loop = mixer.loop;
    setCurrentMixerRangeMin(mixer.range_low);
    setCurrentMixerRangeMax(mixer.range_high);
    return mixer;
  }, [getMixer]);

  const setMixerVolume = useCallback(async (mixer, volumePercent, relative, originalVolume, controlGroup) => {
    const newVolume = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent);
    if (relative) {
      context.trackVolumeObject.volL = context.trackVolumeObject.volL + (newVolume - originalVolume);
      context.trackVolumeObject.volR = context.trackVolumeObject.volR + (newVolume - originalVolume);

      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 != null) {
      let controlGroupsArg;
      if (mixer.mode === MIX_MODES.PERSONAL) {
        controlGroupsArg = 0;
      } else {
        controlGroupsArg = 1;
      }
      await context.jamClient.setSessionMixerCategoryPlayoutState(controlGroup === 'music', controlGroupsArg, context.trackVolumeObject.volL);
    } else {
      await context.jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, context.trackVolumeObject);
    }
  }, []);

  const mediaMixers = useCallback((masterMixer, isOpener, currentAllMixers) => {
    const personalMixer = isOpener ? getMixerByResourceId(masterMixer.rid, MIX_MODES.PERSONAL, currentAllMixers) : masterMixer;
    const personalVuMixer = isOpener ? personalMixer : masterMixer;
    return {
      isOpener: isOpener,
      master: {
        mixer: masterMixer,
        muteMixer: masterMixer,
        vuMixer: masterMixer
      },
      personal: {
        mixer: personalMixer,
        muteMixer: personalMixer,
        vuMixer: personalVuMixer
      }
    };
  }, []);

  const groupTypes = useCallback(() => {
    const localMediaMixers = mixersForGroupIds(mediaTrackGroups, MIX_MODES.MASTER, allMixers);
    const peerLocalMediaMixers = mixersForGroupId(ChannelGroupIds.PeerMediaTrackGroup, MIX_MODES.MASTER, allMixers);

    const newRecordingTrackMixers = [];
    const newBackingTrackMixers = [];
    const newJamTrackMixers = [];
    const newMetronomeTrackMixers = [];
    const newAdhocTrackMixers = [];

    const groupByType = (mixers, isLocalMixer) => {
      for (const mixer of mixers) {
        const mediaType = mixer.media_type;
        const groupId = mixer.group_id;

        if (mediaType === 'MetronomeTrack' || groupId === ChannelGroupIds.MetronomeGroup) {
          newMetronomeTrackMixers.push(mixer);
        } else if (mediaType === null || mediaType === "" || mediaType === 'RecordingTrack') {
          let isJamTrack = false;

          if (mixer.id === session.jamTrackMixdown()?.id) {
            isJamTrack = true;
          }

          if (!isJamTrack && session.jamTracks()) {
            for (const jamTrack of session.jamTracks()) {
              if (mixer.id === jamTrack.id) {
                isJamTrack = true;
                break;
              }
            }
          }

          if (!isJamTrack && session.recordedJamTracks()) {
            for (const recordedJamTrack of session.recordedJamTracks()) {
              if (mixer.id === recordedJamTrack.id) {
                isJamTrack = true;
                break;
              }
            }
          }

          if (isJamTrack) {
            newJamTrackMixers.push(mixer);
          } else {
            let isBackingTrack = false;
            if (session.recordedBackingTracks()) {
              for (const recordedBackingTrack of session.recordedBackingTracks()) {
                if (mixer.id === 'L' + recordedBackingTrack.client_track_id) {
                  isBackingTrack = true;
                  break;
                }
              }
            }

            if (session.backingTracks()) {
              for (const backingTrack of session.backingTracks()) {
                if (mixer.id === 'L' + backingTrack.client_track_id) {
                  isBackingTrack = true;
                  break;
                }
              }
            }

            if (isBackingTrack) {
              newBackingTrackMixers.push(mixer);
            } else {
              newRecordingTrackMixers.push(mixer);
            }
          }
        } else if (mediaType === 'PeerMediaTrack' || mediaType === 'BackingTrack') {
          newBackingTrackMixers.push(mixer);
        } else if (mediaType === 'JamTrack') {
          newJamTrackMixers.push(mixer);
        } else if (mediaType === null || mediaType === "" || mediaType === 'RecordingTrack') {
          newRecordingTrackMixers.push(mixer);
        } else {
          if (mediaType !== 'Broadcast') {
            logger.warn("Unknown track type: " + mediaType);
            newAdhocTrackMixers.push(mixer);
          }
        }
      }
    };

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

    setRecordingTrackMixers(newRecordingTrackMixers);
    setBackingTrackMixers(newBackingTrackMixers);
    setJamTrackMixers(newJamTrackMixers);
    setMetronomeTrackMixers(newMetronomeTrackMixers);
    setAdhocTrackMixers(newAdhocTrackMixers);

    const newBackingTracks = resolveBackingTracks(newBackingTrackMixers, allMixers);
    const newJamTracks = resolveJamTracks(newJamTrackMixers, allMixers);
    const newRecordedTracks = resolveRecordedTracks(newRecordingTrackMixers, allMixers);
    const newMetronome = resolveMetronome(newMetronomeTrackMixers, allMixers);

    setBackingTracks(newBackingTracks);
    setJamTracks(newJamTracks);
    setRecordedTracks(newRecordedTracks);
    setMetronome(newMetronome);

    const newMediaSummary = {
      recordingOpen: newRecordedTracks.length > 0,
      jamTrackOpen: newJamTracks.length > 0,
      backingTrackOpen: newBackingTracks.length > 0,
      metronomeOpen: session.isMetronomeOpen()
    };

    let mediaOpenSummary = false;
    for (const mediaType in newMediaSummary) {
      if (newMediaSummary[mediaType]) {
        mediaOpenSummary = true;
        break;
      }
    }

    newMediaSummary.mediaOpen = mediaOpenSummary;
    newMediaSummary.userNeedsMediaControls = newMediaSummary.mediaOpen || window.JamTrackStore?.jamTrack;
    newMediaSummary.jamTrack = window.JamTrackStore?.jamTrack;

    let isOpener = false;
    if (newMediaSummary.recordingOpen) {
      isOpener = newRecordedTracks[0].isOpener;
    } else if (newMediaSummary.jamTrackOpen) {
      isOpener = newJamTracks[0].isOpener;
    } else if (newMediaSummary.backingTrackOpen) {
      isOpener = newBackingTracks[0].isOpener;
    }

    newMediaSummary.isOpener = isOpener;
    setMediaSummary(newMediaSummary);

    prepareSimulatedMixers(allMixers);
  }, [session]);

  const mixersForGroupId = useCallback((groupId, mixMode, currentAllMixers) => {
    const foundMixers = [];
    const mixers = mixMode === MIX_MODES.MASTER ? masterMixers : personalMixers;
    for (const mixer of mixers) {
      if (mixer.group_id === groupId) {
        foundMixers.push(mixer);
      }
    }

    return foundMixers;
  }, [masterMixers, personalMixers]);

  const getGroupMixer = useCallback((categoryId, mode) => {
    const groupId = mode === MIX_MODES.MASTER ? ChannelGroupIds.MasterCatGroup : ChannelGroupIds.MonitorCatGroup;
    const oppositeGroupId = !mode === MIX_MODES.MASTER ? ChannelGroupIds.MasterCatGroup : ChannelGroupIds.MonitorCatGroup;
    const mixers = mixersForGroupId(groupId, mode, allMixers);
    const oppositeMixers = mixersForGroupId(oppositeGroupId, !mode, allMixers);

    if (mixers.length === 0) {
      return null;
    }

    let found = null;
    let oppositeFound = null;
    for (const mixer of mixers) {
      if (mixer.name === categoryId) {
        found = mixer;
        break;
      }
    }

    for (const mixer of oppositeMixers) {
      if (mixer.name === categoryId) {
        oppositeFound = mixer;
        break;
      }
    }

    if (!found) {
      logger.warn("could not find mixer with categoryId: " + categoryId);
      return null;
    } else {
      return {
        mixer: found,
        muteMixer: found,
        vuMixer: found,
        oppositeMixer: oppositeFound
      };
    }
  }, [mixersForGroupId, allMixers]);

  const mixerForGroupId = useCallback((groupId, mixMode, currentAllMixers) => {
    const mixers = mixersForGroupId(groupId, mixMode, currentAllMixers);
    if (mixers && mixers.length > 0) {
      return mixers[0];
    } else {
      return null;
    }
  }, [mixersForGroupId]);

  const getAudioInputCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.AudioInputMusic, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getChatCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.AudioInputChat, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getUserChatCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.UserChat, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getMediaCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.MediaTrack, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getUserMediaCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.UserMedia, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getUserMusicCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.UserMusic, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getMetronomeCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.Metronome, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getOutputCategoryMixer = useCallback((mode, currentAllMixers) => {
    if (mode === MIX_MODES.MASTER) {
      return getGroupMixer(CategoryGroupIds.MasterCatGroup, mode, currentAllMixers);
    } else {
      return getGroupMixer(CategoryGroupIds.MonitorCatGroup, mode, currentAllMixers);
    }
  }, [getGroupMixer]);

  const getOutputMixer = useCallback((mode, currentAllMixers) => {
    if (mode === MIX_MODES.MASTER) {
      return mixerForGroupId(ChannelGroupIds.MasterGroup, mode, currentAllMixers);
    } else {
      return mixerForGroupId(ChannelGroupIds.MonitorGroup, mode, currentAllMixers);
    }
  }, [mixerForGroupId]);

  const setMixerPan = useCallback(async (mixer, panPercent) => {
    context.trackVolumeObject.pan = context.JK.PanHelpers.convertPercentToPan(panPercent);
    await context.jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, context.trackVolumeObject);
  }, []);

  const resolveBackingTracks = useCallback((currentBackingTrackMixers) => {
    const backingTracks = [];

    if (currentBackingTrackMixers.length === 0) return backingTracks;

    let serverBackingTracks = [];
    let backingTrackMixers = currentBackingTrackMixers;

    if (session.isPlayingRecording()) {
      backingTrackMixers = context._.filter(backingTrackMixers, (mixer) => mixer.managed || !mixer.managed);
      serverBackingTracks = session.recordedBackingTracks();
    } else {
      serverBackingTracks = session.backingTracks();
      backingTrackMixers = context._.filter(backingTrackMixers, (mixer) => !mixer.managed);
      if (backingTrackMixers.length > 1) {
        logger.error("multiple, managed backing track mixers encountered", backingTrackMixers);
        session.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;
      }
    }

    if (!serverBackingTracks || serverBackingTracks.length === 0) {
      return backingTracks;
    }

    let noCorrespondingTracks = false;
    for (const mixer of backingTrackMixers) {
      const correspondingTracks = [];
      noCorrespondingTracks = false;
      if (session.isPlayingRecording()) {
        for (const backingTrack of serverBackingTracks) {
          if (mixer.persisted_track_id === backingTrack.client_track_id || mixer.id === 'L' + backingTrack.client_track_id) {
            correspondingTracks.push(backingTrack);
          }
        }
      } else {
        correspondingTracks.push(serverBackingTracks[0]);
      }

      if (correspondingTracks.length === 0) {
        noCorrespondingTracks = true;
        logger.debug("renderBackingTracks: could not map backing tracks");
        session.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;
      }

      const serverBackingTrack = correspondingTracks[0];

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

      const isOpener = mixer.group_id === ChannelGroupIds.MediaTrackGroup;
      const 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, allMixers)
      };

      backingTracks.push(data);
    }

    return backingTracks;
  }, [session]);

  const resolveJamTracks = useCallback((currentJamTrackMixers) => {
    const _jamTracks = [];

    if (currentJamTrackMixers.length === 0) return _jamTracks;

    let jamTrackMixers = currentJamTrackMixers.slice();
    let jamTracks = [];
    let jamTrackName = null;
    const jamTrackMixdown = session?.jamTrackMixdown() || { id: null };

    if (session?.isPlayingRecording()) {
      jamTracks = session.recordedJamTracks();
      jamTrackName = session.recordedJamTrackName();
    } else {
      jamTracks = session?.jamTracks();
      jamTrackName = session?.jamTrackName();
    }

    const isOpener = jamTrackMixers[0]?.group_id === ChannelGroupIds.JamTrackGroup;

    if (jamTracks) {
      let noCorrespondingTracks = false;

      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);
          // session?.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 {
          const instrumentIcon = context.JK.getInstrumentIcon24('other');
          const part = null;
          const instrumentName = 'Custom Mix';
          const trackName = 'Custom Mix';

          const data = {
            name: jamTrackName,
            trackName: trackName,
            part: part,
            isOpener: isOpener,
            instrumentIcon: instrumentIcon,
            track: jamTrackMixdown,
            mixers: mediaMixers(jamTrackMixers[0], isOpener, allMixers)
          };

          _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 (const jamTrack of jamTracks) {
          let mixer = null;
          const correspondingTracks = [];

          for (const matchMixer of currentJamTrackMixers) {
            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);
            // session?.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;
          }

          jamTrackMixers.splice(jamTrackMixers.indexOf(mixer), 1);

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

          const part = oneOfTheTracks.part;

          let instrumentName = oneOfTheTracks.instrument.description;

          let trackName;
          if (part) {
            trackName = `${instrumentName}: ${part}`;
          } else {
            trackName = instrumentName;
          }

          if (jamTrack.track_type === 'Click') {
            trackName = 'Clicktrack';
          }

          const data = {
            name: jamTrackName,
            trackName: trackName,
            part: part,
            isOpener: isOpener,
            instrumentIcon: instrumentIcon,
            track: oneOfTheTracks,
            mixers: mediaMixers(mixer, isOpener, allMixers)
          };

          _jamTracks.push(data);
        }
      }
    }

    return _jamTracks;
  }, [session, allMixers]);

  const resolveRecordedTracks = useCallback((currentRecordingTrackMixers, currentAllMixers) => {
    const recordedTracks = [];

    if (currentRecordingTrackMixers.length === 0) return recordedTracks;

    const serverRecordedTracks = session?.recordedTracks();

    const isOpener = currentRecordingTrackMixers[0]?.group_id === ChannelGroupIds.MediaTrackGroup;

    if (serverRecordedTracks) {
      const recordingName = session?.recordingName();
      let noCorrespondingTracks = false;
      for (const mixer of currentRecordingTrackMixers) {
        const correspondingTracks = [];
        for (const recordedTrack of 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);
            }
          } else {
            alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id");
          }
        }

        if (correspondingTracks.length === 0) {
          noCorrespondingTracks = true;
          // session?.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;
        }

        const oneOfTheTracks = correspondingTracks[0];
        const instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument_id);
        let userName = oneOfTheTracks.user.name;
        if (!userName) {
          userName = oneOfTheTracks.user.first_name + ' ' + oneOfTheTracks.user.last_name;
        }

        const data = {
          recordingName: recordingName,
          isOpener: isOpener,
          userName: userName,
          instrumentIcon: instrumentIcon,
          track: oneOfTheTracks,
          mixers: mediaMixers(mixer, isOpener, currentAllMixers)
        };

        recordedTracks.push(data);
      }
    }

    return recordedTracks;
  }, [session]);

  const resolveMetronome = useCallback((currentMetronomeTrackMixers, currentAllMixers) => {
    if (currentMetronomeTrackMixers.length === 0) return null;

    const mixer = currentMetronomeTrackMixers[0];

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

    const metronome = {
      instrumentIcon: instrumentIcon,
      mixers: mediaMixers(mixer, true, currentAllMixers)
    };

    return metronome;
  }, []);

  const resolveChatMixer = useCallback((currentMixersByResourceId, currentAllMixers) => {
    const masterChatMixers = mixersForGroupId(ChannelGroupIds.AudioInputChatGroup, MIX_MODES.MASTER, currentAllMixers);

    if (masterChatMixers.length === 0) return null;

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

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

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

    return {
      master: {
        mixer: masterChatMixer,
        muteMixer: masterChatMixer,
        vuMixer: masterChatMixer,
        oppositeMixer: personalChatMixer
      },
      personal: {
        mixer: personalChatMixer,
        muteMixer: personalChatMixer,
        vuMixer: personalChatMixer,
        oppositeMixer: masterChatMixer
      }
    };
  }, []);



  const mixersForGroupIds = useCallback((groupIds, mixMode, currentAllMixers) => {
    const foundMixers = [];
    const mixers = mixMode === MIX_MODES.MASTER ? masterMixers : personalMixers;

    for (const mixer of mixers) {
      for (const groupId of groupIds) {
        if (mixer.group_id === groupId) {
          foundMixers.push(mixer);
        }
      }
    }

    return foundMixers;
  }, [masterMixers, personalMixers]);







  const getMixerByTrackId = useCallback((trackId, mode) => {
    const mixerPair = mixersByTrackId[trackId];

    if (!mixerPair) return null;

    if (mode === undefined) {
      return mixerPair;
    } else {
      if (mode === MIX_MODES.MASTER) {
        return mixerPair.master;
      } else {
        return mixerPair.personal;
      }
    }
  }, [mixersByTrackId]);

  const groupedMixersForClientId = useCallback((clientId, groupIds, usedMixers, mixMode, currentAllMixers) => {
    const foundMixers = {};
    const mixers = mixMode === MIX_MODES.MASTER ? masterMixers : personalMixers;

    for (const mixer of mixers) {
      if (!mixer) {
        continue;
      }

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

    return foundMixers;
  }, [masterMixers, personalMixers]);

  const getMixerByResourceId = useCallback((resourceId, mode) => {
    const mixerPair = mixersByResourceId[resourceId];

    if (!mixerPair) return null;

    if (!mode) {
      return mixerPair;
    } else {
      if (mode === MIX_MODES.MASTER) {
        return mixerPair.master;
      } else {
        return mixerPair.personal;
      }
    }
  }, [mixersByResourceId]);

  const findMixerForTrack = useCallback((client_id, track, myTrack, mode) => {
    let mixer = null;
    let oppositeMixer = null;
    let vuMixer = null;
    let muteMixer = null;

    if (myTrack) {
      mixer = getMixerByTrackId(track.client_track_id, mode);
      vuMixer = mixer;
      muteMixer = mixer;

      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) {
        oppositeMixer = getMixerByTrackId(track.client_track_id, !mode);

        if (mode === MIX_MODES.PERSONAL) {
          muteMixer = oppositeMixer;
        }

        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) {
        case MIX_MODES.MASTER:
          mixer = getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER);

          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) {
            const 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);
            }
          }
          break;

        case MIX_MODES.PERSONAL:
          const mixers = groupedMixersForClientId(client_id, [ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL);
          if (mixers[ChannelGroupIds.UserMusicInputGroup]) {
            mixer = mixers[ChannelGroupIds.UserMusicInputGroup][0];

            vuMixer = mixer;
            muteMixer = mixer;

            if (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 !== ChannelGroupIds.PeerMidiInputMusicGroup) {
                logger.error("personal: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer);
              }
            }
          } else {
            logger.error("no UserMusicInputGroup for client_id #{client_id} in PERSONAL mode", mixers);
          }
          break;
      }
    }

    return {
      mixer: mixer,
      oppositeMixer: oppositeMixer,
      vuMixer: vuMixer,
      muteMixer: muteMixer
    };
  }, [getMixerByTrackId, groupedMixersForClientId, personalMixers, allMixers]);

  const mute = useCallback(async (mixerId, mode, muting) => {
    if (mode == null) { mode = mixMode; }

    const mixer = fillTrackVolumeObject(mixerId, mode);
    if (!mixer) return;

    context.trackVolumeObject.mute = muting;
    await context.jamClient.SessionSetTrackVolumeData(mixerId, mode, context.trackVolumeObject);

    const updatedMixer = getMixer(mixerId, mode);
    updatedMixer.mute = muting;
  }, [mixMode, getMixer, fillTrackVolumeObject]);

  const getOriginalVolume = useCallback((mixers, gainType) => {
    let originalVolume = null;
    if (gainType === 'music') {
      for (const mixer of mixers) {
        if (mixer.name !== CategoryGroupIds.UserMedia && mixer.name !== CategoryGroupIds.MediaTrack) {
          originalVolume = mixer.volume_left;
          break;
        }
      }
    } else {
      originalVolume = mixers[0].volume_left;
    }

    return originalVolume;
  }, []);

  const faderChanged = useCallback(async (data, mixers, gainType, controlGroup) => {
    if (!Array.isArray(mixers)) {
      mixers = [mixers];
    }
    const originalVolume = getOriginalVolume(mixers, gainType);
    if (controlGroup != null) {
      mixers = [mixers[0]];
      for (const m of mixers) {
        const broadcast = !(data.dragging);
        const mixer = fillTrackVolumeObject(m.id, m.mode, allMixers, broadcast);
        if (mixer == null) {
          console.error("MixerHelper: faderChanged: mixer is null, skipping", m, gainType, controlGroup);
          continue;
        }
        const relative = gainType === 'music' && (mixer.name === CategoryGroupIds.UserMedia || mixer.name === CategoryGroupIds.MediaTrack);
        await setMixerVolume(mixer, data.percentage, relative, originalVolume, controlGroup, allMixers);
        const updatedMixer = getMixer(mixer.id, mixer.mode, allMixers);
        updatedMixer.volume_left = context.trackVolumeObject.volL;
      }
    } else {
      for (const m of mixers) {
        const broadcast = !(data.dragging);
        const mixer = fillTrackVolumeObject(m.id, m.mode, allMixers, broadcast);
        if (mixer == null) {
          console.error("MixerHelper: faderChanged: mixer is null, skipping", m, gainType, controlGroup);
          continue;
        }

        const relative = gainType === 'music' && (mixer.name === CategoryGroupIds.UserMedia || mixer.name === CategoryGroupIds.MediaTrack);
        await setMixerVolume(mixer, data.percentage, relative, originalVolume, null, allMixers);
        const updatedMixer = getMixer(mixer.id, mixer.mode, allMixers);
        updatedMixer.volume_left = context.trackVolumeObject.volL;
      }
    }
  }, [getOriginalVolume, getMixer, fillTrackVolumeObject, setMixerVolume, allMixers]);

  const initGain = useCallback((mixer) => {
    if (Array.isArray(mixer)) {
      mixer = mixer[0];
    }

    const gainPercent = context.JK.FaderHelpers.convertAudioTaperToPercent(mixer.volume_left);
    context.JK.FaderHelpers.setFaderValue(mixer.id, gainPercent);
    context.JK.FaderHelpers.showFader(mixer.id);
  }, []);

  const panChanged = useCallback(async (data, mixers, groupId) => {
    if (!Array.isArray(mixers)) { mixers = [mixers]; }
    const result = [];
    for (const mixer of mixers) {
      const broadcast = !(data.dragging);
      const filledMixer = fillTrackVolumeObject(mixer.id, mixer.mode, allMixers, broadcast);

      await setMixerPan(filledMixer, data.percentage);

      const updatedMixer = getMixer(filledMixer.id, filledMixer.mode, allMixers);
      updatedMixer.pan = context.trackVolumeObject.pan;
      result.push(context.trackVolumeObject.pan);
    }
    return result;
  }, [getMixer, fillTrackVolumeObject, setMixerPan, allMixers]);

  const initPan = useCallback((mixer) => {
    const panPercent = context.JK.PanHelpers.convertPanToPercent(mixer.pan);
    context.JK.FaderHelpers.setFaderValue(mixer.id, panPercent, Math.abs(mixer.pan));
    context.JK.FaderHelpers.showFader(mixer.id);
  }, []);



  const loopChanged = useCallback(async (mixer, shouldLoop) => {
    fillTrackVolumeObject(mixer.id, mixer.mode, allMixers);
    context.trackVolumeObject.loop = shouldLoop;
    await context.jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, context.trackVolumeObject);

    const updatedMixer = getMixer(mixer.id, mixer.mode, allMixers);
    updatedMixer.loop = context.trackVolumeObject.loop;
  }, [getMixer, fillTrackVolumeObject, allMixers]);



  const percentFromMixerValue = useCallback((min, max, value) => {
    try {
      const range = Math.abs(max - min);
      const magnitude = value - min;
      const percent = Math.round(100 * (magnitude / range));
      return percent;
    } catch (err) {
      return 0;
    }
  }, []);

  const percentToMixerValue = useCallback((min, max, percent) => {
    const range = Math.abs(max - min);
    const multiplier = percent / 100;
    let value = min + (multiplier * range);

    if (value < min) {
      value = min;
    }

    if (value > max) {
      value = max;
    }

    return value;
  }, []);



  const collectStats = useCallback((mixer, currentVuStats) => {
    let mixerStats = currentVuStats[mixer.id];

    if (!mixerStats) {
      mixerStats = { count: 0, group_name: context.JK.groupIdDisplay(mixer) };
      currentVuStats[mixer.id] = mixerStats;
    }

    mixerStats.count++;
  }, []);

  const dumpVUStats = useCallback((currentVuStats) => {
    logger.debug("VU STAT DUMP");
    for (const mixerId in currentVuStats) {
      const mixerStat = currentVuStats[mixerId];
      logger.debug("VU STAT: #{mixerStat.group_name} count=#{mixerStat.count}");
    }
  }, []);

  const updateVU = useCallback((mixerId, mode, leftValue, leftClipping, rightValue, rightClipping) => {
    const mixer = getMixer(mixerId, mode);
    if (mixer) {
      if (shouldCollectVuStats) collectStats(mixer, vuStats);
      context.JK.VuHelpers.updateVU3(mixer, leftValue, leftClipping, rightValue, rightClipping);
    }
  }, [getMixer, shouldCollectVuStats, collectStats, vuStats]);

  const getTrackInfo = useCallback(async () => {
    return await context.JK.TrackHelpers.getTrackInfo(context.jamClient, masterMixers);
  }, [masterMixers]);



  const prepareSimulatedMixers = useCallback(() => {
    const newSimulatedMusicCategoryMixers = {};
    const newSimulatedChatCategoryMixers = {};

    newSimulatedMusicCategoryMixers[MIX_MODES.MASTER] = getSimulatedMusicCategoryMixer(MIX_MODES.MASTER, allMixers);
    newSimulatedMusicCategoryMixers[MIX_MODES.PERSONAL] = getSimulatedMusicCategoryMixer(MIX_MODES.PERSONAL, allMixers);
    newSimulatedChatCategoryMixers[MIX_MODES.MASTER] = getSimulatedChatCategoryMixer(MIX_MODES.MASTER, allMixers);
    newSimulatedChatCategoryMixers[MIX_MODES.PERSONAL] = getSimulatedChatCategoryMixer(MIX_MODES.PERSONAL, allMixers);

    setSimulatedMusicCategoryMixers(newSimulatedMusicCategoryMixers);
    setSimulatedChatCategoryMixers(newSimulatedChatCategoryMixers);
  }, [allMixers]);

  const getSimulatedMusicCategoryMixer = useCallback((mode, currentAllMixers) => {
    const myInputs = getAudioInputCategoryMixer(mode, currentAllMixers)?.mixer;
    const peerInputs = getUserMusicCategoryMixer(mode, currentAllMixers)?.mixer;
    const myMedia = getMediaCategoryMixer(mode, currentAllMixers)?.mixer;
    const peerMedia = getUserMediaCategoryMixer(mode, currentAllMixers)?.mixer;
    const metronome = getMetronomeCategoryMixer(mode, currentAllMixers)?.mixer;
    const output = getOutputMixer(mode, currentAllMixers);
    const oppositeOutput = getOutputMixer(!mode, currentAllMixers);

    if (myInputs) {
      return {
        first: myInputs,
        mixer: [myInputs, peerInputs, myMedia, peerMedia, metronome],
        muteMixer: [myInputs, peerInputs, myMedia, peerMedia, metronome],
        vuMixer: output
      };
    } else {
      return null;
    }
  }, [getAudioInputCategoryMixer, getUserMusicCategoryMixer, getMediaCategoryMixer, getUserMediaCategoryMixer, getMetronomeCategoryMixer, getOutputMixer]);

  const getSimulatedChatCategoryMixer = useCallback((mode, currentAllMixers) => {
    const myChats = getChatCategoryMixer(mode, currentAllMixers)?.mixer;
    const peerChats = getUserChatCategoryMixer(mode, currentAllMixers)?.mixer;
    if (myChats) {
      return {
        first: myChats,
        mixer: [myChats, peerChats],
        muteMixer: [myChats, peerChats],
        vuMixer: myChats
      };
    } else {
      return null;
    }
  }, []);



  const refreshMixer = useCallback((mixers, currentAllMixers) => {
    if (!mixers || !mixers.mixer) return null;

    let updateMixers = null;
    if (Array.isArray(mixers.mixer)) {
      if (mixers.mixer.length > 0) {
        updateMixers = [];
        for (const mixer of mixers.mixer) {
          updateMixers.push(getMixer(mixer.id, mixer.mode, currentAllMixers));
        }
      }
    } else {
      updateMixers = getMixer(mixers.mixer.id, mixers.mixer.mode, currentAllMixers);
    }

    let updatedVUMixers = null;
    if (Array.isArray(mixers.vuMixer)) {
      updatedVUMixers = [];
      for (const vuMixer of mixers.vuMixer) {
        updatedVUMixers.push(getMixer(vuMixer.id, vuMixer.mode, currentAllMixers));
      }
    } else {
      updatedVUMixers = getMixer(mixers.vuMixer.id, mixers.vuMixer.mode, currentAllMixers);
    }

    let updateMuteMixers = null;
    if (Array.isArray(mixers.muteMixer)) {
      updateMuteMixers = [];
      for (const muteMixer of mixers.muteMixer) {
        updateMuteMixers.push(getMixer(muteMixer.id, muteMixer.mode, currentAllMixers));
      }
    } else {
      updateMuteMixers = getMixer(mixers.muteMixer.id, mixers.muteMixer.mode, currentAllMixers);
    }

    const oppositeMixer = mixers.oppositeMixer ? getMixer(mixers.oppositeMixer.id, mixers.oppositeMixer.mode, currentAllMixers) : null;

    if (updateMixers) {
      return {
        mixer: updateMixers,
        vuMixer: updatedVUMixers,
        muteMixer: updateMuteMixers,
        oppositeMixer: oppositeMixer
      };
    } else {
      return null;
    }
  }, [getMixer]);

  const recordingName = useCallback(() => {
    return session.recordingName();
  }, [session]);

  const jamTrackName = useCallback(() => {
    return session.jamTrackName();
  }, [session]);

  return {
    // Initialization
    initialize,

    // Core state
    mixMode,
    session,
    masterMixers,
    personalMixers,
    mixersByResourceId,
    mixersByTrackId,
    allMixers,
    currentMixerRangeMin,
    currentMixerRangeMax,
    mediaSummary,
    mediaTrackGroups,
    muteBothMasterAndPersonalGroups,
    vuStats,
    shouldCollectVuStats,
    simulatedMusicCategoryMixers,
    simulatedChatCategoryMixers,
    vuMeterUpdatePrefMap,
    recordingTrackMixers,
    backingTrackMixers,
    jamTrackMixers,
    metronomeTrackMixers,
    adhocTrackMixers,
    backingTracks,
    jamTracks,
    recordedTracks,
    metronome,
    chatMixer,

    // Methods
    organize,
    groupTypes,
    resolveBackingTracks,
    resolveJamTracks,
    resolveRecordedTracks,
    resolveMetronome,
    // resolveChatMixer,
    // mediaMixers,
    mixersForGroupIds,
    mixersForGroupId,
    mixerForGroupId,
    getMixer,
    getMixerByTrackId,
    groupedMixersForClientId,
    getMixerByResourceId,
    findMixerForTrack,
    mute,
    getOriginalVolume,
    faderChanged,
    initGain,
    panChanged,
    initPan,
    setMixerPan,
    loopChanged,
    setMixerVolume,
    percentFromMixerValue,
    percentToMixerValue,
    fillTrackVolumeObject,
    collectStats,
    dumpVUStats,
    updateVU,
    getTrackInfo,
    getGroupMixer,
    prepareSimulatedMixers,
    getSimulatedMusicCategoryMixer,
    getSimulatedChatCategoryMixer,
    getAudioInputCategoryMixer,
    getChatCategoryMixer,
    getUserChatCategoryMixer,
    getMediaCategoryMixer,
    getUserMediaCategoryMixer,
    getUserMusicCategoryMixer,
    getMetronomeCategoryMixer,
    getOutputCategoryMixer,
    getOutputMixer,
    refreshMixer,
    recordingName,
    jamTrackName,
    update
  };
};

export default useMixerHelper;
