package funkin.ui.debug.charting.toolboxes;

import funkin.audio.SoundGroup;
import haxe.ui.components.Button;
import haxe.ui.components.HorizontalSlider;
import haxe.ui.components.Label;
import flixel.addons.display.FlxTiledSprite;
import flixel.math.FlxMath;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
import haxe.ui.backend.flixel.components.SpriteWrapper;
import funkin.ui.debug.charting.commands.SetAudioOffsetCommand;
import funkin.ui.haxeui.components.WaveformPlayer;
import funkin.audio.waveform.WaveformDataParser;
import haxe.ui.containers.VBox;
import haxe.ui.containers.Absolute;
import haxe.ui.containers.ScrollView;
import haxe.ui.containers.Frame;
import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;

/**
 * The toolbox which allows modifying information like Song Title, Scroll Speed, Characters/Stages, and starting BPM.
 */
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.

@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/offsets.xml"))
class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox
{
  var waveformContainer:Absolute;
  var waveformScrollview:ScrollView;
  var waveformPlayer:WaveformPlayer;
  var waveformOpponent:WaveformPlayer;
  var waveformInstrumental:WaveformPlayer;
  var offsetButtonZoomIn:Button;
  var offsetButtonZoomOut:Button;
  var offsetButtonPause:Button;
  var offsetButtonPlay:Button;
  var offsetButtonStop:Button;
  var offsetStepperPlayer:NumberStepper;
  var offsetStepperOpponent:NumberStepper;
  var offsetStepperInstrumental:NumberStepper;
  var offsetTicksContainer:Absolute;
  var playheadSprite:SpriteWrapper;

  static final TICK_LABEL_X_OFFSET:Float = 4.0;

  static final PLAYHEAD_RIGHT_PAD:Float = 10.0;

  static final BASE_SCALE:Float = 64.0;
  static final MIN_SCALE:Float = 4.0;
  static final WAVEFORM_ZOOM_MULT:Float = 1.5;

  static final MAGIC_SCALE_BASE_TIME:Float = 5.0;

  var waveformScale:Float = BASE_SCALE;

  var playheadAbsolutePos(get, set):Float;

  function get_playheadAbsolutePos():Float
  {
    return playheadSprite.left;
  }

  function set_playheadAbsolutePos(value:Float):Float
  {
    return playheadSprite.left = value;
  }

  var playheadRelativePos(get, set):Float;

  function get_playheadRelativePos():Float
  {
    return playheadSprite.left - waveformScrollview.hscrollPos;
  }

  function set_playheadRelativePos(value:Float):Float
  {
    return playheadSprite.left = waveformScrollview.hscrollPos + value;
  }

  /**
   * The amount you need to multiply the zoom by such that, at the base zoom level, one tick is equal to `MAGIC_SCALE_BASE_TIME` seconds.
   */
  var waveformMagicFactor:Float = 1.0;

  var audioPreviewTracks:SoundGroup;

  var tickTiledSprite:FlxTiledSprite;

  var tickLabels:Array<Label> = [];

  // Local store of the audio offsets, so we can detect when they change.
  var audioPreviewPlayerOffset:Float = 0;
  var audioPreviewOpponentOffset:Float = 0;
  var audioPreviewInstrumentalOffset:Float = 0;

  public function new(chartEditorState2:ChartEditorState)
  {
    super(chartEditorState2);

    initialize();

    this.onDialogClosed = onClose;
  }

  function onClose(event:UIEvent)
  {
    chartEditorState.menubarItemToggleToolboxOffsets.selected = false;
  }

  function initialize():Void
  {
    // Starting position.
    // TODO: Save and load this.
    this.x = 150;
    this.y = 250;

    offsetPlayerVolume.onChange = (_) -> {
      var targetVolume = offsetPlayerVolume.value * 2 / 100;
      setTrackVolume(PLAYER, targetVolume);
    };
    offsetPlayerMute.onClick = (_) -> {
      toggleMuteTrack(PLAYER);
    };
    offsetPlayerSolo.onClick = (_) -> {
      soloTrack(PLAYER);
    };
    offsetOpponentVolume.onChange = (_) -> {
      var targetVolume = offsetOpponentVolume.value * 2 / 100;
      setTrackVolume(OPPONENT, targetVolume);
    };
    offsetOpponentMute.onClick = (_) -> {
      toggleMuteTrack(OPPONENT);
    };
    offsetOpponentSolo.onClick = (_) -> {
      soloTrack(OPPONENT);
    };
    offsetInstrumentalVolume.onChange = (_) -> {
      var targetVolume = offsetInstrumentalVolume.value * 2 / 100;
      setTrackVolume(INSTRUMENTAL, targetVolume);
    };
    offsetInstrumentalMute.onClick = (_) -> {
      toggleMuteTrack(INSTRUMENTAL);
    };
    offsetInstrumentalSolo.onClick = (_) -> {
      soloTrack(INSTRUMENTAL);
    };
    offsetButtonZoomIn.onClick = (_) -> {
      zoomWaveformIn();
    };
    offsetButtonZoomOut.onClick = (_) -> {
      zoomWaveformOut();
    };
    offsetButtonPause.onClick = (_) -> {
      pauseAudioPreview();
    };
    offsetButtonPlay.onClick = (_) -> {
      playAudioPreview();
    };
    offsetButtonStop.onClick = (_) -> {
      stopAudioPreview();
    };
    offsetStepperPlayer.onChange = (event:UIEvent) -> {
      if (event.value == chartEditorState.currentVocalOffsetPlayer) return;
      if (dragWaveform != null) return;

      chartEditorState.performCommand(new SetAudioOffsetCommand(PLAYER, event.value));
      refresh();
    }
    offsetStepperOpponent.onChange = (event:UIEvent) -> {
      if (event.value == chartEditorState.currentVocalOffsetOpponent) return;
      if (dragWaveform != null) return;

      chartEditorState.performCommand(new SetAudioOffsetCommand(OPPONENT, event.value));
      refresh();
    }
    offsetStepperInstrumental.onChange = (event:UIEvent) -> {
      if (event.value == chartEditorState.currentInstrumentalOffset) return;
      if (dragWaveform != null) return;

      chartEditorState.performCommand(new SetAudioOffsetCommand(INSTRUMENTAL, event.value));
      refresh();
    }
    waveformScrollview.onScroll = (_) -> {
      if (!audioPreviewTracks.playing)
      {
        // Move the playhead if it would go out of view.
        var prevPlayheadRelativePos = playheadRelativePos;
        playheadRelativePos = FlxMath.bound(playheadRelativePos, 0, waveformScrollview.width - PLAYHEAD_RIGHT_PAD);
        var diff = playheadRelativePos - prevPlayheadRelativePos;

        if (diff != 0)
        {
          // We have to change the song time to match the playhead position when we move it.
          var currentWaveformIndex:Int = Std.int(playheadAbsolutePos * (waveformScale / BASE_SCALE * waveformMagicFactor));
          var targetSongTimeSeconds:Float = waveformPlayer.waveform.waveformData.indexToSeconds(currentWaveformIndex);
          audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC;
        }

        addOffsetsToAudioPreview();
      }
      else
      {
        // The scrollview probably changed because the song position changed.
        // If we try to move the song now it will glitch.
      }

      // Either way, clipRect has changed, so we need to refresh the waveforms.
      refresh();
    };

    initializeTicks();

    refreshAudioPreview();
    refresh();
    refreshTicks();

    waveformPlayer.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
      onStartDragWaveform(PLAYER);
    });
    waveformOpponent.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
      onStartDragWaveform(OPPONENT);
    });
    waveformInstrumental.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
      onStartDragWaveform(INSTRUMENTAL);
    });

    offsetTicksContainer.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
      onStartDragPlayhead();
    });
  }

  function initializeTicks():Void
  {
    tickTiledSprite = new FlxTiledSprite(chartEditorState.offsetTickBitmap, 100, chartEditorState.offsetTickBitmap.height, true, false);
    offsetTicksSprite.sprite = tickTiledSprite;
    tickTiledSprite.width = 5000;
  }

  /**
   * Pull the audio tracks from the chart editor state and create copies of them to play in the Offsets Toolbox.
   * These must be DEEP CLONES or else the editor will affect the audio preview!
   */
  public function refreshAudioPreview():Void
  {
    if (audioPreviewTracks == null)
    {
      audioPreviewTracks = new SoundGroup();
      // Make sure audioPreviewTracks (and all its children) receives update() calls.
      chartEditorState.add(audioPreviewTracks);
    }
    else
    {
      audioPreviewTracks.stop();
      audioPreviewTracks.clear();
    }

    var instTrack = chartEditorState.audioInstTrack.clone();
    audioPreviewTracks.add(instTrack);

    var playerVoice = chartEditorState.audioVocalTrackGroup.getPlayerVoice();
    if (playerVoice != null) audioPreviewTracks.add(playerVoice.clone());

    var opponentVoice = chartEditorState.audioVocalTrackGroup.getOpponentVoice();
    if (opponentVoice != null) audioPreviewTracks.add(opponentVoice.clone());

    // Build player waveform.
    // waveformPlayer.waveform.forceUpdate = true;
    waveformPlayer.waveform.waveformData = playerVoice?.waveformData;
    // Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it.
    waveformPlayer.waveform.duration = (playerVoice?.length ?? 1000.0) / Constants.MS_PER_SEC;

    // Build opponent waveform.
    // waveformOpponent.waveform.forceUpdate = true;
    // note: if song only has one set of vocals (Vocals.ogg/mp3) then this is null and crashes charting editor
    // so we null check
    waveformOpponent.waveform.waveformData = opponentVoice?.waveformData;
    waveformOpponent.waveform.duration = (opponentVoice?.length ?? 1000.0) / Constants.MS_PER_SEC;

    // Build instrumental waveform.
    // waveformInstrumental.waveform.forceUpdate = true;
    waveformInstrumental.waveform.waveformData = chartEditorState.audioInstTrack.waveformData;
    waveformInstrumental.waveform.duration = (instTrack?.length ?? 1000.0) / Constants.MS_PER_SEC;

    addOffsetsToAudioPreview();
  }

  public function refreshTicks():Void
  {
    while (tickLabels.length > 0)
    {
      var label = tickLabels.pop();
      offsetTicksContainer.removeComponent(label);
    }

    var labelYPos:Float = chartEditorState.offsetTickBitmap.height / 2;
    var labelHeight:Float = chartEditorState.offsetTickBitmap.height / 2;

    var numberOfTicks:Int = Math.floor(waveformInstrumental.waveform.width / chartEditorState.offsetTickBitmap.width * 2) + 1;

    for (index in 0...numberOfTicks)
    {
      var tickPos = chartEditorState.offsetTickBitmap.width / 2 * index;
      var tickTime = tickPos * (waveformScale / BASE_SCALE * waveformMagicFactor) / waveformInstrumental.waveform.waveformData.pointsPerSecond();

      var tickLabel:Label = new Label();
      tickLabel.text = formatTime(tickTime);
      tickLabel.styleNames = "offset-ticks-label";
      tickLabel.height = labelHeight;
      // Positioning within offsetTicksContainer is absolute (relative to the container itself).
      tickLabel.top = labelYPos;
      tickLabel.left = tickPos + TICK_LABEL_X_OFFSET;

      offsetTicksContainer.addComponent(tickLabel);
      tickLabels.push(tickLabel);
    }
  }

  function formatTime(seconds:Float):String
  {
    if (seconds <= 0) return "0.0";

    var integerSeconds = Math.floor(seconds);
    var decimalSeconds = Math.floor((seconds - integerSeconds) * 10);

    if (integerSeconds < 60)
    {
      return '${integerSeconds}.${decimalSeconds}';
    }
    else
    {
      var integerMinutes = Math.floor(integerSeconds / 60);
      var remainingSeconds = integerSeconds % 60;
      var remainingSecondsPad:String = remainingSeconds < 10 ? '0$remainingSeconds' : '$remainingSeconds';

      return '${integerMinutes}:${remainingSecondsPad}${decimalSeconds > 0 ? '.$decimalSeconds' : ''}';
    }
  }

  function buildTickLabel():Void {}

  public function onStartDragPlayhead():Void
  {
    Screen.instance.registerEvent(MouseEvent.MOUSE_MOVE, onDragPlayhead);
    Screen.instance.registerEvent(MouseEvent.MOUSE_UP, onStopDragPlayhead);

    movePlayheadToMouse();
  }

  public function onDragPlayhead(event:MouseEvent):Void
  {
    movePlayheadToMouse();
  }

  public function onStopDragPlayhead(event:MouseEvent):Void
  {
    // Stop dragging.
    Screen.instance.unregisterEvent(MouseEvent.MOUSE_MOVE, onDragPlayhead);
    Screen.instance.unregisterEvent(MouseEvent.MOUSE_UP, onStopDragPlayhead);
  }

  function movePlayheadToMouse():Void
  {
    // Determine the position of the mouse relative to the
    var mouseXPos = FlxG.mouse.x;

    var relativeMouseXPos = mouseXPos - waveformScrollview.screenX;
    var targetPlayheadPos = relativeMouseXPos + waveformScrollview.hscrollPos;

    // Move the playhead to the mouse position.
    playheadAbsolutePos = targetPlayheadPos;

    // Move the audio preview to the playhead position.
    var currentWaveformIndex:Int = Std.int(playheadAbsolutePos * (waveformScale / BASE_SCALE * waveformMagicFactor));
    var targetSongTimeSeconds:Float = waveformPlayer.waveform.waveformData.indexToSeconds(currentWaveformIndex);
    audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC;
  }

  public function onStartDragWaveform(waveform:Waveform):Void
  {
    dragMousePosition = FlxG.mouse.x;
    dragWaveform = waveform;

    Screen.instance.registerEvent(MouseEvent.MOUSE_MOVE, onDragWaveform);
    Screen.instance.registerEvent(MouseEvent.MOUSE_UP, onStopDragWaveform);
  }

  var dragMousePosition:Float = 0;
  var dragWaveform:Waveform = null;
  var dragOffsetMs:Float = 0;

  public function onDragWaveform(event:MouseEvent):Void
  {
    var newDragMousePosition = FlxG.mouse.x;
    var deltaMousePosition = newDragMousePosition - dragMousePosition;

    if (deltaMousePosition == 0) return;

    var deltaPixels:Float = deltaMousePosition * (waveformScale / BASE_SCALE * waveformMagicFactor);
    var deltaMilliseconds:Float = switch (dragWaveform)
    {
      case PLAYER:
        deltaPixels / waveformPlayer.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC;
      case OPPONENT:
        deltaPixels / waveformOpponent.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC;
      case INSTRUMENTAL:
        deltaPixels / waveformInstrumental.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC;
    };

    switch (dragWaveform)
    {
      case PLAYER:
        // chartEditorState.currentVocalOffsetPlayer += deltaMilliseconds;
        dragOffsetMs += deltaMilliseconds;
        offsetStepperPlayer.value += deltaMilliseconds;
      case OPPONENT:
        // chartEditorState.currentVocalOffsetOpponent += deltaMilliseconds;
        dragOffsetMs += deltaMilliseconds;
        offsetStepperOpponent.value += deltaMilliseconds;
      case INSTRUMENTAL:
        // chartEditorState.currentInstrumentalOffset += deltaMilliseconds;
        dragOffsetMs += deltaMilliseconds;
        offsetStepperInstrumental.value += deltaMilliseconds;
    }

    dragMousePosition = newDragMousePosition;

    refresh();
  }

  public function onStopDragWaveform(event:MouseEvent):Void
  {
    // Stop dragging.
    Screen.instance.unregisterEvent(MouseEvent.MOUSE_MOVE, onDragWaveform);
    Screen.instance.unregisterEvent(MouseEvent.MOUSE_UP, onStopDragWaveform);

    // Apply the offset change after dragging happens.
    // We only do this once per drag so we don't get 20 commands a second in the history.
    if (dragOffsetMs != 0)
    {
      // false to not refresh this toolbox, we will manually do that later.
      switch (dragWaveform)
      {
        case PLAYER:
          chartEditorState.performCommand(new SetAudioOffsetCommand(PLAYER, chartEditorState.currentVocalOffsetPlayer + dragOffsetMs, false));
        case OPPONENT:
          chartEditorState.performCommand(new SetAudioOffsetCommand(OPPONENT, chartEditorState.currentVocalOffsetOpponent + dragOffsetMs, false));
        case INSTRUMENTAL:
          chartEditorState.performCommand(new SetAudioOffsetCommand(INSTRUMENTAL, chartEditorState.currentInstrumentalOffset + dragOffsetMs, false));
      }
    }

    dragOffsetMs = 0;
    dragMousePosition = 0;
    dragWaveform = null;

    refresh();
    addOffsetsToAudioPreview();
  }

  public function playAudioPreview():Void
  {
    audioPreviewTracks.play(false, audioPreviewTracks.time);
  }

  public function addOffsetsToAudioPreview():Void
  {
    var trackInst = audioPreviewTracks.members[0];
    if (trackInst != null)
    {
      audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset;
      trackInst.time -= audioPreviewInstrumentalOffset;
    }

    var trackPlayer = audioPreviewTracks.members[1];
    if (trackPlayer != null)
    {
      audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer;
      trackPlayer.time -= audioPreviewPlayerOffset;
    }

    var trackOpponent = audioPreviewTracks.members[2];
    if (trackOpponent != null)
    {
      audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent;
      trackOpponent.time -= audioPreviewOpponentOffset;
    }
  }

  public function pauseAudioPreview():Void
  {
    audioPreviewTracks.pause();
  }

  public function stopAudioPreview():Void
  {
    audioPreviewTracks.stop();

    audioPreviewTracks.time = 0;

    var trackInst = audioPreviewTracks.members[0];
    if (trackInst != null)
    {
      audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset;
      trackInst.time = -audioPreviewInstrumentalOffset;
    }

    var trackPlayer = audioPreviewTracks.members[1];
    if (trackPlayer != null)
    {
      audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer;
      trackPlayer.time = -audioPreviewPlayerOffset;
    }

    var trackOpponent = audioPreviewTracks.members[2];
    if (trackOpponent != null)
    {
      audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent;
      trackOpponent.time = -audioPreviewOpponentOffset;
    }

    waveformScrollview.hscrollPos = 0;
    playheadAbsolutePos = 0 + playheadSprite.width;
    refresh();
    addOffsetsToAudioPreview();
  }

  public function zoomWaveformIn():Void
  {
    if (waveformScale > MIN_SCALE)
    {
      waveformScale = waveformScale / WAVEFORM_ZOOM_MULT;
      if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE;

      // Update the playhead too!
      playheadAbsolutePos = playheadAbsolutePos * WAVEFORM_ZOOM_MULT;

      // Recenter the scroll view on the playhead.
      var vaguelyCenterPlayheadOffset = waveformScrollview.width / 8;
      waveformScrollview.hscrollPos = playheadAbsolutePos - vaguelyCenterPlayheadOffset;

      refresh();
      refreshTicks();
    }
    else
    {
      waveformScale = MIN_SCALE;
    }
  }

  public function zoomWaveformOut():Void
  {
    waveformScale = waveformScale * WAVEFORM_ZOOM_MULT;
    if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE;

    // Update the playhead too!
    playheadAbsolutePos = playheadAbsolutePos / WAVEFORM_ZOOM_MULT;

    // Recenter the scroll view on the playhead.
    var vaguelyCenterPlayheadOffset = waveformScrollview.width / 8;
    waveformScrollview.hscrollPos = playheadAbsolutePos - vaguelyCenterPlayheadOffset;

    refresh();
    refreshTicks();
  }

  public function setTrackVolume(target:Waveform, volume:Float):Void
  {
    switch (target)
    {
      case Waveform.INSTRUMENTAL:
        var trackInst = audioPreviewTracks.members[0];
        if (trackInst != null)
        {
          trackInst.volume = volume;
        }
      case Waveform.PLAYER:
        var trackPlayer = audioPreviewTracks.members[1];
        if (trackPlayer != null)
        {
          trackPlayer.volume = volume;
        }
      case Waveform.OPPONENT:
        var trackOpponent = audioPreviewTracks.members[2];
        if (trackOpponent != null)
        {
          trackOpponent.volume = volume;
        }
    }
  }

  public function muteTrack(target:Waveform):Void
  {
    switch (target)
    {
      case Waveform.INSTRUMENTAL:
        var trackInst = audioPreviewTracks.members[0];
        if (trackInst != null)
        {
          trackInst.muted = true;
          offsetInstrumentalMute.text = trackInst.muted ? "Unmute" : "Mute";
        }
      case Waveform.PLAYER:
        var trackPlayer = audioPreviewTracks.members[1];
        if (trackPlayer != null)
        {
          trackPlayer.muted = true;
          offsetPlayerMute.text = trackPlayer.muted ? "Unmute" : "Mute";
        }
      case Waveform.OPPONENT:
        var trackOpponent = audioPreviewTracks.members[2];
        if (trackOpponent != null)
        {
          trackOpponent.muted = true;
          offsetOpponentMute.text = trackOpponent.muted ? "Unmute" : "Mute";
        }
    }
  }

  public function unmuteTrack(target:Waveform):Void
  {
    switch (target)
    {
      case Waveform.INSTRUMENTAL:
        var trackInst = audioPreviewTracks.members[0];
        if (trackInst != null)
        {
          trackInst.muted = false;
          offsetInstrumentalMute.text = trackInst.muted ? "Unmute" : "Mute";
        }
      case Waveform.PLAYER:
        var trackPlayer = audioPreviewTracks.members[1];
        if (trackPlayer != null)
        {
          trackPlayer.muted = false;
          offsetPlayerMute.text = trackPlayer.muted ? "Unmute" : "Mute";
        }
      case Waveform.OPPONENT:
        var trackOpponent = audioPreviewTracks.members[2];
        if (trackOpponent != null)
        {
          trackOpponent.muted = false;
          offsetOpponentMute.text = trackOpponent.muted ? "Unmute" : "Mute";
        }
    }
  }

  public function toggleMuteTrack(target:Waveform):Void
  {
    switch (target)
    {
      case Waveform.INSTRUMENTAL:
        var trackInst = audioPreviewTracks.members[0];
        if (trackInst != null)
        {
          trackInst.muted = !trackInst.muted;
          offsetInstrumentalMute.text = trackInst.muted ? "Unmute" : "Mute";
        }
      case Waveform.PLAYER:
        var trackPlayer = audioPreviewTracks.members[1];
        if (trackPlayer != null)
        {
          trackPlayer.muted = !trackPlayer.muted;
          offsetPlayerMute.text = trackPlayer.muted ? "Unmute" : "Mute";
        }
      case Waveform.OPPONENT:
        var trackOpponent = audioPreviewTracks.members[2];
        if (trackOpponent != null)
        {
          trackOpponent.muted = !trackOpponent.muted;
          offsetOpponentMute.text = trackOpponent.muted ? "Unmute" : "Mute";
        }
    }
  }

  /**
   * Clicking the solo button will unmute the track and mute all other tracks.
   * @param target
   */
  public function soloTrack(target:Waveform):Void
  {
    switch (target)
    {
      case Waveform.PLAYER:
        muteTrack(Waveform.OPPONENT);
        muteTrack(Waveform.INSTRUMENTAL);
        unmuteTrack(Waveform.PLAYER);
      case Waveform.OPPONENT:
        muteTrack(Waveform.PLAYER);
        muteTrack(Waveform.INSTRUMENTAL);
        unmuteTrack(Waveform.OPPONENT);
      case Waveform.INSTRUMENTAL:
        muteTrack(Waveform.PLAYER);
        muteTrack(Waveform.OPPONENT);
        unmuteTrack(Waveform.INSTRUMENTAL);
    }
  }

  public override function update(elapsed:Float)
  {
    super.update(elapsed);

    if (audioPreviewTracks.playing)
    {
      trace('Playback time: ${audioPreviewTracks.time}');

      var targetScrollPos:Float = waveformInstrumental.waveform.waveformData.secondsToIndex(audioPreviewTracks.time / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor);
      // waveformScrollview.hscrollPos = targetScrollPos;
      var prevPlayheadAbsolutePos = playheadAbsolutePos;
      playheadAbsolutePos = targetScrollPos;
      var playheadDiff = playheadAbsolutePos - prevPlayheadAbsolutePos;

      // BEHAVIOR A.
      // Just move the scroll view with the playhead, constraining it so that the playhead is always visible.
      // waveformScrollview.hscrollPos += playheadDiff;
      // waveformScrollview.hscrollPos = FlxMath.bound(waveformScrollview.hscrollPos, playheadAbsolutePos - playheadSprite.width, playheadAbsolutePos);

      // BEHAVIOR B.
      // Keep `playheadAbsolutePos` within the bounds of the screen.
      // The scroll view will eventually move to where the playhead is 1/8th of the way from the left. This looks kinda nice!
      // TODO: This causes a hard snap to scroll when the playhead is to the right of the playheadCenterPoint.
      // var playheadCenterPoint = waveformScrollview.width / 8;
      // waveformScrollview.hscrollPos = FlxMath.bound(waveformScrollview.hscrollPos, playheadAbsolutePos - playheadCenterPoint, playheadAbsolutePos);

      // playheadRelativePos = 0;

      // BEHAVIOR C.
      // Copy Audacity!
      // If the playhead is out of view, jump forward or backward by one screen width until it's in view.
      if (playheadAbsolutePos < waveformScrollview.hscrollPos)
      {
        waveformScrollview.hscrollPos -= waveformScrollview.width;
      }
      if (playheadAbsolutePos > waveformScrollview.hscrollPos + waveformScrollview.width)
      {
        waveformScrollview.hscrollPos += waveformScrollview.width;
      }
    }

    if (chartEditorState.currentInstrumentalOffset != audioPreviewInstrumentalOffset)
    {
      var track = audioPreviewTracks.members[0];
      if (track != null)
      {
        track.time += audioPreviewInstrumentalOffset;
        track.time -= chartEditorState.currentInstrumentalOffset;
        audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset;
      }
    }
    if (chartEditorState.currentVocalOffsetPlayer != audioPreviewPlayerOffset)
    {
      var track = audioPreviewTracks.members[1];
      if (track != null)
      {
        track.time += audioPreviewPlayerOffset;
        track.time -= chartEditorState.currentVocalOffsetPlayer;
        audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer;
      }
    }
    if (chartEditorState.currentVocalOffsetOpponent != audioPreviewOpponentOffset)
    {
      var track = audioPreviewTracks.members[2];
      if (track != null)
      {
        track.time += audioPreviewOpponentOffset;
        track.time -= chartEditorState.currentVocalOffsetOpponent;
        audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent;
      }
    }
    offsetLabelTime.text = formatTime(audioPreviewTracks.time / Constants.MS_PER_SEC);
    // Keep the playhead in view.
    // playheadRelativePos = FlxMath.bound(playheadRelativePos, waveformScrollview.hscrollPos + 1,
    //   Math.min(waveformScrollview.hscrollPos + waveformScrollview.width, waveformContainer.width));
  }

  public override function refresh():Void
  {
    super.refresh();

    waveformMagicFactor = MAGIC_SCALE_BASE_TIME / (chartEditorState.offsetTickBitmap.width / waveformInstrumental.waveform.waveformData.pointsPerSecond());

    var currentZoomFactor = waveformScale / BASE_SCALE * waveformMagicFactor;

    var maxWidth:Int = -1;

    offsetStepperPlayer.value = chartEditorState.currentVocalOffsetPlayer;
    offsetStepperOpponent.value = chartEditorState.currentVocalOffsetOpponent;
    offsetStepperInstrumental.value = chartEditorState.currentInstrumentalOffset;

    waveformPlayer.waveform.time = -chartEditorState.currentVocalOffsetPlayer / Constants.MS_PER_SEC; // Negative offsets make the song start early.
    waveformPlayer.waveform.width = (waveformPlayer.waveform.waveformData?.length ?? 1000) / currentZoomFactor;
    if (waveformPlayer.waveform.width > maxWidth) maxWidth = Std.int(waveformPlayer.waveform.width);
    waveformPlayer.waveform.height = 65;

    waveformOpponent.waveform.time = -chartEditorState.currentVocalOffsetOpponent / Constants.MS_PER_SEC;
    waveformOpponent.waveform.width = (waveformOpponent.waveform.waveformData?.length ?? 1000) / currentZoomFactor;
    if (waveformOpponent.waveform.width > maxWidth) maxWidth = Std.int(waveformOpponent.waveform.width);
    waveformOpponent.waveform.height = 65;

    waveformInstrumental.waveform.time = -chartEditorState.currentInstrumentalOffset / Constants.MS_PER_SEC;
    waveformInstrumental.waveform.width = (waveformInstrumental.waveform.waveformData?.length ?? 1000) / currentZoomFactor;
    if (waveformInstrumental.waveform.width > maxWidth) maxWidth = Std.int(waveformInstrumental.waveform.width);
    waveformInstrumental.waveform.height = 65;

    // Live update the drag, but don't actually change the underlying offset until we release the mouse to finish dragging.
    if (dragWaveform != null) switch (dragWaveform)
    {
      case PLAYER:
        // chartEditorState.currentVocalOffsetPlayer += deltaMilliseconds;
        waveformPlayer.waveform.time -= dragOffsetMs / Constants.MS_PER_SEC;
        offsetStepperPlayer.value += dragOffsetMs;
      case OPPONENT:
        // chartEditorState.currentVocalOffsetOpponent += deltaMilliseconds;
        waveformOpponent.waveform.time -= dragOffsetMs / Constants.MS_PER_SEC;
        offsetStepperOpponent.value += dragOffsetMs;
      case INSTRUMENTAL:
        // chartEditorState.currentInstrumentalOffset += deltaMilliseconds;
        waveformInstrumental.waveform.time -= dragOffsetMs / Constants.MS_PER_SEC;
        offsetStepperInstrumental.value += dragOffsetMs;
      default:
        // No drag, no
    }

    waveformPlayer.waveform.markDirty();
    waveformOpponent.waveform.markDirty();
    waveformInstrumental.waveform.markDirty();

    waveformContainer.width = maxWidth;
    tickTiledSprite.width = maxWidth;
  }

  public static function build(chartEditorState:ChartEditorState):ChartEditorOffsetsToolbox
  {
    return new ChartEditorOffsetsToolbox(chartEditorState);
  }
}

enum Waveform
{
  PLAYER;
  OPPONENT;
  INSTRUMENTAL;
}