package funkin.ui.debug.charting.toolboxes;

import flixel.addons.display.FlxTiledSprite;
import flixel.math.FlxMath;
import funkin.audio.SoundGroup;
import funkin.audio.waveform.WaveformDataParser;
import funkin.ui.debug.charting.commands.SetFreeplayPreviewCommand;
import funkin.ui.haxeui.components.WaveformPlayer;
import funkin.ui.freeplay.FreeplayState;
import funkin.util.TimerUtil;
import haxe.ui.backend.flixel.components.SpriteWrapper;
import haxe.ui.components.Button;
import haxe.ui.components.HorizontalSlider;
import haxe.ui.components.Label;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
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/freeplay.xml"))
class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox
{
  var waveformContainer:Absolute;
  var waveformScrollview:ScrollView;
  var waveformMusic:WaveformPlayer;
  var freeplayButtonZoomIn:Button;
  var freeplayButtonZoomOut:Button;
  var freeplayButtonPause:Button;
  var freeplayButtonPlay:Button;
  var freeplayButtonStop:Button;
  var freeplayPreviewStart:NumberStepper;
  var freeplayPreviewEnd:NumberStepper;
  var freeplayTicksContainer:Absolute;
  var playheadSprite:SpriteWrapper;
  var previewSelectionSprite: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 STARTING_SCALE:Float = 1024.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 = STARTING_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;
  }

  var previewBoxStartPosAbsolute(get, set):Float;

  function get_previewBoxStartPosAbsolute():Float
  {
    return previewSelectionSprite.left;
  }

  function set_previewBoxStartPosAbsolute(value:Float):Float
  {
    return previewSelectionSprite.left = value;
  }

  var previewBoxEndPosAbsolute(get, set):Float;

  function get_previewBoxEndPosAbsolute():Float
  {
    return previewSelectionSprite.left + previewSelectionSprite.width;
  }

  function set_previewBoxEndPosAbsolute(value:Float):Float
  {
    if (value < previewBoxStartPosAbsolute) return previewSelectionSprite.left = previewBoxStartPosAbsolute;
    return previewSelectionSprite.width = value - previewBoxStartPosAbsolute;
  }

  var previewBoxStartPosRelative(get, set):Float;

  function get_previewBoxStartPosRelative():Float
  {
    return previewSelectionSprite.left - waveformScrollview.hscrollPos;
  }

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

  var previewBoxEndPosRelative(get, set):Float;

  function get_previewBoxEndPosRelative():Float
  {
    return previewSelectionSprite.left + previewSelectionSprite.width - waveformScrollview.hscrollPos;
  }

  function set_previewBoxEndPosRelative(value:Float):Float
  {
    if (value < previewBoxStartPosRelative) return previewSelectionSprite.left = previewBoxStartPosRelative;
    return previewSelectionSprite.width = value - previewBoxStartPosRelative;
  }

  /**
   * 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 freeplayPreviewVolume(get, null):Float;

  function get_freeplayPreviewVolume():Float
  {
    return freeplayMusicVolume.value * 2 / 100;
  }

  var tickLabels:Array<Label> = [];

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

    initialize();

    this.onDialogClosed = onClose;
  }

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

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

    freeplayMusicVolume.onChange = (_) -> {
      setTrackVolume(freeplayPreviewVolume);
    };
    freeplayMusicMute.onClick = (_) -> {
      toggleMuteTrack();
    };
    freeplayButtonZoomIn.onClick = (_) -> {
      zoomWaveformIn();
    };
    freeplayButtonZoomOut.onClick = (_) -> {
      zoomWaveformOut();
    };
    freeplayButtonPause.onClick = (_) -> {
      pauseAudioPreview();
    };
    freeplayButtonPlay.onClick = (_) -> {
      playAudioPreview();
    };
    freeplayButtonStop.onClick = (_) -> {
      stopAudioPreview();
    };
    testPreview.onClick = (_) -> {
      performPreview();
    };
    freeplayPreviewStart.onChange = (event:UIEvent) -> {
      if (event.value == chartEditorState.currentSongFreeplayPreviewStart) return;
      if (waveformDragStartPos != null) return; // The values are changing because we are dragging the preview.

      chartEditorState.performCommand(new SetFreeplayPreviewCommand(event.value, null));
      refresh();
    }
    freeplayPreviewEnd.onChange = (event:UIEvent) -> {
      if (event.value == chartEditorState.currentSongFreeplayPreviewEnd) return;
      if (waveformDragStartPos != null) return; // The values are changing because we are dragging the preview.

      chartEditorState.performCommand(new SetFreeplayPreviewCommand(null, 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);
        trace('newPos: ${playheadRelativePos}');
        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 = waveformMusic.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();

    waveformMusic.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
      onStartDragWaveform();
    });

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

  function initializeTicks():Void
  {
    tickTiledSprite = new FlxTiledSprite(chartEditorState.offsetTickBitmap, 100, chartEditorState.offsetTickBitmap.height, true, false);
    freeplayTicksSprite.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.
    // waveformMusic.waveform.forceUpdate = true;
    var perfStart:Float = TimerUtil.start();
    var waveformData1 = playerVoice?.waveformData;
    var waveformData2 = opponentVoice?.waveformData ?? playerVoice?.waveformData; // this null check is for songs that only have 1 vocals file!
    var waveformData3 = chartEditorState.audioInstTrack.waveformData;
    var waveformData = waveformData3.merge(waveformData1).merge(waveformData2);
    trace('Waveform data merging took: ${TimerUtil.seconds(perfStart)}');

    waveformMusic.waveform.waveformData = waveformData;
    // Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it.
    waveformMusic.waveform.duration = instTrack.length / Constants.MS_PER_SEC;

    addOffsetsToAudioPreview();
  }

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

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

    var numberOfTicks:Int = Math.floor(waveformMusic.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) / waveformMusic.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;

      freeplayTicksContainer.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 = waveformMusic.waveform.waveformData.indexToSeconds(currentWaveformIndex);
    audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC;
  }

  var waveformDragStartPos:Null<Float> = null;

  var waveformDragPreviewStartPos:Float;
  var waveformDragPreviewEndPos:Float;

  public function onStartDragWaveform():Void
  {
    waveformDragStartPos = FlxG.mouse.x;

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

  public function onDragWaveform(event:MouseEvent):Void
  {
    // Set waveformDragPreviewStartPos and waveformDragPreviewEndPos to the position the drag started and the current mouse position.
    // This only affects the visuals.

    var currentAbsMousePos = FlxG.mouse.x;
    var dragDiff = currentAbsMousePos - waveformDragStartPos;

    var currentRelativeMousePos = currentAbsMousePos - waveformScrollview.screenX;
    var relativeStartPos = waveformDragStartPos - waveformScrollview.screenX;

    var isDraggingRight = dragDiff > 0;
    var hasDraggedEnough = Math.abs(dragDiff) > 10;

    if (hasDraggedEnough)
    {
      if (isDraggingRight)
      {
        waveformDragPreviewStartPos = relativeStartPos;
        waveformDragPreviewEndPos = currentRelativeMousePos;
      }
      else
      {
        waveformDragPreviewStartPos = currentRelativeMousePos;
        waveformDragPreviewEndPos = relativeStartPos;
      }
    }

    refresh();
  }

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

    var previewStartPosAbsolute = waveformDragPreviewStartPos + waveformScrollview.hscrollPos;
    var previewStartPosIndex:Int = Std.int(previewStartPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor));
    var previewStartPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewStartPosIndex) * Constants.MS_PER_SEC);

    var previewEndPosAbsolute = waveformDragPreviewEndPos + waveformScrollview.hscrollPos;
    var previewEndPosIndex:Int = Std.int(previewEndPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor));
    var previewEndPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewEndPosIndex) * Constants.MS_PER_SEC);

    chartEditorState.performCommand(new SetFreeplayPreviewCommand(previewStartPosMs, previewEndPosMs));

    waveformDragStartPos = null;
    waveformDragPreviewStartPos = 0;
    waveformDragPreviewEndPos = 0;

    refresh();
    addOffsetsToAudioPreview();
  }

  public function playAudioPreview():Void
  {
    if (isPerformingPreview) stopPerformingPreview();

    audioPreviewTracks.volume = freeplayPreviewVolume;
    audioPreviewTracks.play(false, audioPreviewTracks.time);
  }

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

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

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

  public function pauseAudioPreview():Void
  {
    if (isPerformingPreview) stopPerformingPreview();

    audioPreviewTracks.pause();
  }

  public function stopAudioPreview():Void
  {
    if (isPerformingPreview) stopPerformingPreview();

    audioPreviewTracks.stop();

    audioPreviewTracks.time = 0;

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

  public function zoomWaveformIn():Void
  {
    if (isPerformingPreview) stopPerformingPreview();

    if (waveformScale > MIN_SCALE)
    {
      waveformScale = waveformScale / WAVEFORM_ZOOM_MULT;
      if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE;

      trace('Zooming in, scale: ${waveformScale}');

      // 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;

    trace('Zooming out, scale: ${waveformScale}');

    // 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(volume:Float):Void
  {
    audioPreviewTracks.volume = volume;
  }

  public function muteTrack():Void
  {
    audioPreviewTracks.muted = true;
  }

  public function unmuteTrack():Void
  {
    audioPreviewTracks.muted = false;
  }

  public function toggleMuteTrack():Void
  {
    audioPreviewTracks.muted = !audioPreviewTracks.muted;
  }

  var isPerformingPreview:Bool = false;
  var isFadingOutPreview:Bool = false;

  public function performPreview():Void
  {
    isPerformingPreview = true;
    isFadingOutPreview = false;
    audioPreviewTracks.play(true, chartEditorState.currentSongFreeplayPreviewStart);
    audioPreviewTracks.fadeIn(FreeplayState.FADE_IN_DURATION, FreeplayState.FADE_IN_START_VOLUME * freeplayPreviewVolume,
      FreeplayState.FADE_IN_END_VOLUME * freeplayPreviewVolume, null);
  }

  public function stopPerformingPreview():Void
  {
    isPerformingPreview = false;
    isFadingOutPreview = false;
    audioPreviewTracks.volume = freeplayPreviewVolume;
    audioPreviewTracks.pause();
  }

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

    if (isPerformingPreview && !audioPreviewTracks.playing)
    {
      stopPerformingPreview();
    }

    if (isPerformingPreview && audioPreviewTracks.playing)
    {
      var startFadeOutTime = chartEditorState.currentSongFreeplayPreviewEnd - (FreeplayState.FADE_OUT_DURATION * Constants.MS_PER_SEC);
      trace('startFadeOutTime: ${audioPreviewTracks.time} >= ${startFadeOutTime}');
      if (!isFadingOutPreview && audioPreviewTracks.time >= startFadeOutTime)
      {
        isFadingOutPreview = true;
        audioPreviewTracks.fadeOut(FreeplayState.FADE_OUT_DURATION, FreeplayState.FADE_OUT_END_VOLUME * freeplayPreviewVolume, (_) -> {
          trace('Stop performing preview! ${audioPreviewTracks.time}');
          stopPerformingPreview();
        });
      }
    }

    if (audioPreviewTracks.playing)
    {
      var targetScrollPos:Float = waveformMusic.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 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;
      }
    }
    freeplayLabelTime.text = formatTime(audioPreviewTracks.time / Constants.MS_PER_SEC);
    if (waveformDragStartPos != null && (waveformDragPreviewStartPos > 0 && waveformDragPreviewEndPos > 0))
    {
      var previewStartPosAbsolute = waveformDragPreviewStartPos + waveformScrollview.hscrollPos;
      var previewStartPosIndex:Int = Std.int(previewStartPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor));
      var previewStartPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewStartPosIndex) * Constants.MS_PER_SEC);

      var previewEndPosAbsolute = waveformDragPreviewEndPos + waveformScrollview.hscrollPos;
      var previewEndPosIndex:Int = Std.int(previewEndPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor));
      var previewEndPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewEndPosIndex) * Constants.MS_PER_SEC);

      // Set the values in milliseconds.
      freeplayPreviewStart.value = previewStartPosMs;
      freeplayPreviewEnd.value = previewEndPosMs;

      previewBoxStartPosAbsolute = previewStartPosAbsolute;
      previewBoxEndPosAbsolute = previewEndPosAbsolute;
    }
    else
    {
      previewBoxStartPosAbsolute = waveformMusic.waveform.waveformData.secondsToIndex(chartEditorState.currentSongFreeplayPreviewStart / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor);
      previewBoxEndPosAbsolute = waveformMusic.waveform.waveformData.secondsToIndex(chartEditorState.currentSongFreeplayPreviewEnd / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor);

      freeplayPreviewStart.value = chartEditorState.currentSongFreeplayPreviewStart;
      freeplayPreviewEnd.value = chartEditorState.currentSongFreeplayPreviewEnd;
    }
  }

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

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

    var currentZoomFactor = waveformScale / BASE_SCALE * waveformMagicFactor;

    var maxWidth:Int = -1;

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

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

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