import React, { useRef, useEffect, useState, useCallback, useLayoutEffect, useMemo } from 'react';
import './PianoRoll.css';
import { useProjectStore } from '../../stores/projectStore';
import { FaGripLines } from 'react-icons/fa';
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
import type { KGAudioRegion } from '../../core/region/KGAudioRegion';
import { DEBUG_MODE, PIANO_ROLL_CONSTANTS, TOOLBAR_CONSTANTS } from '../../constants';
import PianoRollHeader from './PianoRollHeader';
import PianoRollToolbar from './PianoRollToolbar';
import NoteAttributeBar from './NoteAttributeBar';
import PianoRollContent from './PianoRollContent';
import { KGCore } from '../../core/KGCore';
import { KGMidiNote } from '../../core/midi/KGMidiNote';
import { KGMidiTrack, type InstrumentType } from '../../core/track/KGMidiTrack';
import { KGPianoRollState } from '../../core/state/KGPianoRollState';
import { ConfigManager } from '../../core/config/ConfigManager';
import { beatsToBar } from '../../util/midiUtil';
import { UpdateRegionCommand } from '../../core/commands';
import { getSuitableChords, noteNameToPitchClass } from '../../util/scaleUtil';
import { showAlert, showPrompt } from '../../util/dialogUtil';
import {
  normalizeSpectrogramHeightResolution,
  type SpectrogramHeightResolution,
} from '../../util/spectrogramUtil';
import type { PianoRollAutomationType } from './pianoRollAutomation';
import type { SheetMeasureMetric } from './sheetNotationTypes';
import { getSheetPlayheadPixel, getSheetQuantizationOptions, parseSheetQuantization } from './sheetNotation';

type RegionPlayheadRelation = 'before' | 'inside' | 'after';
type ViewportSwitchAlignment = 'center' | 'region-start' | 'region-end';
type ViewportClampScope = 'region' | 'track';

interface PendingModeSwitchRequest {
  sourceSheetMusicViewEnabled: boolean;
  destinationSheetMusicViewEnabled: boolean;
  destinationSheetMusicTrackScopeEnabled: boolean;
  alignment: ViewportSwitchAlignment;
  anchorBeat: number;
  clampScope: ViewportClampScope;
}

interface ModeSwitchRequestOptions {
  playheadBeat: number;
  regionStartBeat: number;
  regionEndBeat: number;
  sourceSheetMusicViewEnabled: boolean;
  destinationSheetMusicViewEnabled: boolean;
  destinationSheetMusicTrackScopeEnabled: boolean;
}

interface ScrollLeftForViewportRequestOptions {
  request: PendingModeSwitchRequest;
  container: HTMLDivElement;
  sheetMeasureMetrics: SheetMeasureMetric[];
  activeRegionStartBeat: number;
  activeRegionEndBeat: number;
  songEndBeat: number;
}

interface PianoRollProps {
  onClose: () => void;
  regionId: string | null;
  initialPosition?: { x: number; y: number };
  initialSize?: { width: number; height: number };
  mode?: 'midi-edit' | 'spectrogram' | 'hybrid';
  requestedSheetMusicViewEnabled?: boolean;
  pianoRollViewRequestVersion?: number;
  audioRegion?: KGAudioRegion;
  trackId?: string;
  projectName?: string;
}

const PianoRoll: React.FC<PianoRollProps> = ({
  onClose,
  regionId,
  initialPosition,
  initialSize,
  mode = 'midi-edit',
  requestedSheetMusicViewEnabled = false,
  pianoRollViewRequestVersion = 0,
  audioRegion,
  trackId,
  projectName,
}) => {
  const isSpectrogram = mode === 'spectrogram';
  const isHybrid = mode === 'hybrid';
  const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showKGOnePanel, showEventListPanel, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm, pianoRollScrollRequest, selectedNoteIds, automationRedrawVersion } = useProjectStore();

  // Tool state for piano roll
  const [activeTool, setActiveTool] = useState<'pointer' | 'pencil'>('pointer');

  // Spectrogram controls (only used in spectrogram mode)
  const [spectrogramThresholdDb, setSpectrogramThresholdDb] = useState<number>(-25);
  const [spectrogramPower, setSpectrogramPower] = useState<number>(0.5);
  const [spectrogramHeightResolution, setSpectrogramHeightResolution] =
    useState<SpectrogramHeightResolution>(3);

  // Piano roll zoom (1x–8x); updates --region-grid-beat-width CSS variable
  const [pianoRollZoom, setPianoRollZoom] = useState<number>(() => KGPianoRollState.instance().getPianoRollZoom());
  const [automationEnabled, setAutomationEnabled] = useState(false);
  const [automationType, setAutomationType] = useState<PianoRollAutomationType>('pitch-bend');
  const [sheetMusicViewEnabled, setSheetMusicViewEnabled] = useState(false);
  const [sheetMusicTrackScopeEnabled, setSheetMusicTrackScopeEnabled] = useState(false);
  const [sheetQuantization, setSheetQuantization] = useState('16,48');
  const [sheetMeasureMetrics, setSheetMeasureMetrics] = useState<SheetMeasureMetric[]>([]);

  // Quantization state
  const [quantPosition, setQuantPosition] = useState<string>('1/8');
  const [quantLength, setQuantLength] = useState<string>('1/8');

  // Snapping state
  const [snapping, setSnapping] = useState<string>('NO SNAP');

  // Chord guide state
  const [chordGuide, setChordGuide] = useState<string>('N');

  // Piano roll state with temporary initial values
  const [position, setPosition] = useState(initialPosition || { x: 0, y: 0 });

  // Blink effect state for toolbar button feedback
  const [blinkButton, setBlinkButton] = useState<string | null>(null);
  const [size, setSize] = useState(initialSize || { width: 800, height: PIANO_ROLL_CONSTANTS.PIANO_ROLL_HEIGHT });
  const [isDragging, setIsDragging] = useState(false);
  const [isResizing, setIsResizing] = useState(false);
  const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
  const [activeRegion, setActiveRegion] = useState<KGMidiRegion | null>(null);

  const selectedNotes = useMemo(
    () => activeRegion?.getNotes().filter(n => selectedNoteIds.includes(n.getId())) ?? [],
    [activeRegion, selectedNoteIds]
  );
  const parentMidiTrack = useMemo(() => {
    if (!activeRegion) {
      return null;
    }

    return tracks.find(track => track.getId().toString() === activeRegion.getTrackId()) ?? null;
  }, [activeRegion, tracks]);
  const activeInstrument = useMemo<InstrumentType>(() => (
    parentMidiTrack instanceof KGMidiTrack ? parentMidiTrack.getInstrument() : 'acoustic_grand_piano'
  ), [parentMidiTrack]);
  const parsedSheetQuantization = useMemo(
    () => parseSheetQuantization(sheetQuantization),
    [sheetQuantization]
  );

  const pianoRollRef = useRef<HTMLDivElement>(null);
  const pianoRollContentRef = useRef<HTMLDivElement>(null);
  const pianoRollNoteScrollRef = useRef<HTMLDivElement>(null);
  const pianoGridRef = useRef<HTMLDivElement>(null);
  const wasDraggingRef = useRef<boolean>(false);

  // Refs for auto-scroll during playback
  const pianoRollExpectedScrollLeftRef = useRef<number>(-1);
  const pianoRollIsPlayingRef = useRef(false);
  const pendingZoomAnchorBeatRef = useRef<number | null>(null);
  const pendingModeSwitchRequestRef = useRef<PendingModeSwitchRequest | null>(null);
  const previousSheetMusicViewEnabledRef = useRef<boolean>(false);
  const previousActiveRegionIdRef = useRef<string | null>(null);
  const lastAppliedViewRequestVersionRef = useRef<number>(0);

  // Ref for storing the setNoteUpdateCounter function
  const triggerNoteUpdateRef = useRef<React.Dispatch<React.SetStateAction<number>> | null>(null);

  // Ref for storing the deleteSelectedNotes function
  const deleteSelectedNotesRef = useRef<(() => boolean) | null>(null);

  // Calculate initial position and size once on mount
  useEffect(() => {
    // Skip if initialPosition or initialSize were provided as props
    if (!initialPosition || !initialSize) {
      // Calculate initial position based on window dimensions
      const calculateInitialPosition = () => {
        // Dynamically get heights from CSS computed styles
        const statusBarElement = document.querySelector('.status-bar');

        // Get actual heights from DOM elements, or use fallback values if elements don't exist yet
        const statusBarHeight = statusBarElement ? statusBarElement.clientHeight : 30;
        const pianoRollHeight = PIANO_ROLL_CONSTANTS.PIANO_ROLL_HEIGHT;

        // Compute left offset when instrument selection panel is open
        const rootStyles = getComputedStyle(document.documentElement);
        const instrumentPanelWidthStr = rootStyles.getPropertyValue('--instrument-selection-width') || '300px';
        const instrumentPanelWidth = parseInt(instrumentPanelWidthStr, 10) || 300;

        if (DEBUG_MODE.PIANO_ROLL) {
          console.log(`Positioning piano roll with heights - statusBar: ${statusBarHeight}px, pianoRoll: ${pianoRollHeight}px`);
        }

        return {
          x: showInstrumentSelection ? instrumentPanelWidth : 0,
          y: window.innerHeight - statusBarHeight - pianoRollHeight
        };
      };

      const calculateInitialSize = () => {
        const rootStyles = getComputedStyle(document.documentElement);
        const chatBoxWidthStr = rootStyles.getPropertyValue('--chat-box-width') || '350px';
        const instrumentPanelWidthStr = rootStyles.getPropertyValue('--instrument-selection-width') || '300px';
        const chatBoxWidth = parseInt(chatBoxWidthStr, 10) || 350;
        const instrumentPanelWidth = parseInt(instrumentPanelWidthStr, 10) || 300;

        let availableWidth = window.innerWidth;
        if (showChatBox || showKGOnePanel || showEventListPanel) availableWidth -= chatBoxWidth;
        if (showInstrumentSelection) availableWidth -= instrumentPanelWidth;

        // Ensure a sensible minimum starting width
        const clampedWidth = Math.max(400, availableWidth);

        return {
          width: clampedWidth,
          height: PIANO_ROLL_CONSTANTS.PIANO_ROLL_HEIGHT
        };
      };

      // Set position and size only if not provided as props
      if (!initialPosition) {
        setPosition(calculateInitialPosition());
      }

      if (!initialSize) {
        setSize(calculateInitialSize());
      }
    }
    // Intentionally run once on mount to capture layout at open time
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // Empty dependency array means this runs once on mount

  // Find and set the active region when regionId changes
  useEffect(() => {
    if (!regionId) {
      setActiveRegion(null);
      return;
    }

    let found: KGMidiRegion | null = null;
    for (const track of tracks) {
      const region = track.getRegions().find(r => r.getId() === regionId);
      if (region && region instanceof KGMidiRegion) {
        found = region;
        break;
      }
    }

    // Always update — clears stale MIDI region when switching to an audio region
    setActiveRegion(found);

    if (found && DEBUG_MODE.PIANO_ROLL) {
      console.log(`Active region set in PianoRoll: ${found.getId()}`);
      console.log(`Region details: name=${found.getName()}, trackId=${found.getTrackId()}, trackIndex=${found.getTrackIndex()}`);
    }
  }, [regionId, tracks]);

  // Sync local state with KGPianoRollState on mount
  useEffect(() => {
    const pianoRollState = KGPianoRollState.instance();

    // Sync snapping state
    const currentSnap = pianoRollState.getCurrentSnap();
    setSnapping(currentSnap);

    // Sync tool state
    const currentTool = pianoRollState.getActiveTool() as 'pointer' | 'pencil';
    setActiveTool(currentTool);
    setAutomationEnabled(pianoRollState.getAutomationViewEnabled());
    setAutomationType(pianoRollState.getCurrentAutomationType() as PianoRollAutomationType);
    setSheetMusicViewEnabled(pianoRollState.getSheetMusicViewEnabled());
    setSheetMusicTrackScopeEnabled(pianoRollState.getSheetMusicTrackScopeEnabled());
    setSheetQuantization(pianoRollState.getSheetQuantization());

    if (DEBUG_MODE.PIANO_ROLL) {
      console.log(`Synced piano roll state on mount - snap: ${currentSnap}, tool: ${currentTool}`);
    }
  }, []); // Empty dependency array means this runs once on mount

  useEffect(() => {
    if (isSpectrogram) {
      return;
    }

    if (pianoRollViewRequestVersion === 0 || lastAppliedViewRequestVersionRef.current === pianoRollViewRequestVersion) {
      return;
    }

    lastAppliedViewRequestVersionRef.current = pianoRollViewRequestVersion;

    if (activeRegion) {
      pendingModeSwitchRequestRef.current = createPendingModeSwitchRequest({
        playheadBeat: playheadPosition,
        regionStartBeat: activeRegion.getStartFromBeat(),
        regionEndBeat: activeRegion.getStartFromBeat() + activeRegion.getLength(),
        sourceSheetMusicViewEnabled: sheetMusicViewEnabled,
        destinationSheetMusicViewEnabled: requestedSheetMusicViewEnabled,
        destinationSheetMusicTrackScopeEnabled: requestedSheetMusicViewEnabled && sheetMusicTrackScopeEnabled,
      });
    } else {
      pendingModeSwitchRequestRef.current = null;
    }

    setSheetMusicViewEnabled(requestedSheetMusicViewEnabled);
    KGPianoRollState.instance().setSheetMusicViewEnabled(requestedSheetMusicViewEnabled);
  }, [
    activeRegion,
    isSpectrogram,
    pianoRollViewRequestVersion,
    playheadPosition,
    requestedSheetMusicViewEnabled,
    sheetMusicTrackScopeEnabled,
    sheetMusicViewEnabled,
  ]);

  useEffect(() => {
    let unsubscribe: (() => void) | undefined;

    const initializeSpectrogramHeightResolution = async () => {
      const configManager = ConfigManager.instance();
      if (!configManager.getIsInitialized()) {
        await configManager.initialize();
      }

      const applyResolutionFromConfig = () => {
        setSpectrogramHeightResolution(
          normalizeSpectrogramHeightResolution(
            configManager.get('editor.spectrogram_height_resolution')
          )
        );
      };

      applyResolutionFromConfig();
      unsubscribe = configManager.addChangeListener((changedKeys) => {
        if (
          changedKeys.includes('__all__') ||
          changedKeys.includes('editor.spectrogram_height_resolution')
        ) {
          applyResolutionFromConfig();
        }
      });
    };

    void initializeSpectrogramHeightResolution();

    return () => {
      unsubscribe?.();
    };
  }, []);

  // Add keyboard event listener for Escape
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      // Close on Escape key
      if (event.key === 'Escape') {
        if (DEBUG_MODE.PIANO_ROLL) {
          console.log('Closing piano roll with ESC key');
        }
        onClose();
      }
    };

    // Add event listener
    window.addEventListener('keydown', handleKeyDown);

    // Remove event listener on cleanup
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [onClose]);

  // Handle mouse events for dragging and resizing
  const handleMouseDown = (e: React.MouseEvent, action: 'drag' | 'resize') => {
    if (action === 'drag') {
      setIsDragging(true);
      wasDraggingRef.current = false; // Reset the dragging flag
      if (pianoRollRef.current) {
        const rect = pianoRollRef.current.getBoundingClientRect();
        setDragOffset({
          x: e.clientX - rect.left,
          y: e.clientY - rect.top
        });
      }
    } else if (action === 'resize') {
      setIsResizing(true);
      e.preventDefault();
    }
  };

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      if (isDragging) {
        // Set the flag to true as soon as any movement happens
        wasDraggingRef.current = true;

        setPosition({
          x: e.clientX - dragOffset.x,
          y: e.clientY - dragOffset.y
        });
      } else if (isResizing) {
        setSize({
          width: Math.max(400, e.clientX - position.x),
          height: Math.max(300, e.clientY - position.y)
        });
      }
    };

    const handleMouseUp = () => {
      setIsDragging(false);
      setIsResizing(false);
      // We keep wasDraggingRef.current as is - it will be used in handleTitleClick
      // and reset on the next mousedown
    };

    if (isDragging || isResizing) {
      document.addEventListener('mousemove', handleMouseMove as unknown as EventListener);
      document.addEventListener('mouseup', handleMouseUp);
    }

    return () => {
      document.removeEventListener('mousemove', handleMouseMove as unknown as EventListener);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [isDragging, isResizing, dragOffset, position]);

  // Handle title click to rename the region
  const handleTitleClick = async () => {
    // If we were just dragging, don't show the rename dialog
    if (wasDraggingRef.current) {
      if (DEBUG_MODE.PIANO_ROLL) {
        console.log("Skipping rename dialog because the window was just dragged");
      }
      return;
    }

    if (!activeRegion) return;

    // Show a prompt to get the new name
    const newName = await showPrompt("Enter a new name for the region:", activeRegion.getName());

    // If the user clicked Cancel or entered an empty string, do nothing
    if (!newName || newName.trim() === '' || newName === activeRegion.getName()) return;

    // Use command pattern to update the region name with undo support
    try {
      const command = new UpdateRegionCommand(activeRegion.getId(), { name: newName.trim() });
      KGCore.instance().executeCommand(command);

      if (DEBUG_MODE.PIANO_ROLL) {
        console.log(`Executed UpdateRegionCommand: renamed region ${activeRegion.getId()} to "${newName}" using command pattern`);
      }

      // Update the store to trigger re-render
      const updatedTracks = [...tracks];
      useProjectStore.setState({ tracks: updatedTracks });

    } catch (error) {
      console.error('Error renaming region:', error);
      await showAlert('Failed to rename region. Please try again.');
    }
  };

  // Handle tool selection
  const handleToolSelect = (tool: 'pointer' | 'pencil') => {
    setActiveTool(tool);
    KGPianoRollState.instance().setActiveTool(tool);
    if (DEBUG_MODE.PIANO_ROLL) {
      console.log(`Selected tool: ${tool}`);
    }
  };

  // Handle snapping selection
  const handleSnappingSelect = useCallback((value: string) => {
    setSnapping(value);
    KGPianoRollState.instance().setCurrentSnap(value);
    if (DEBUG_MODE.PIANO_ROLL) {
      console.log(`Selected snapping: ${value}`);
    }
  }, []);

  // Handle mode selection
  const handleModeSelect = useCallback((value: string) => {
    setSelectedMode(value);
    if (DEBUG_MODE.PIANO_ROLL) {
      console.log(`Selected mode: ${value}`);
    }
  }, [setSelectedMode]);

  // Handle chord guide selection
  const handleChordGuideSelect = useCallback((value: string) => {
    setChordGuide(value);
  }, []);

  // Update suitable chords whenever chord guide, key signature, or mode changes
  useEffect(() => {
    const pianoRollState = KGPianoRollState.instance();

    if (chordGuide === 'N') {
      // Disabled - clear chord data
      pianoRollState.setCurrentSuitableChords({});
      pianoRollState.setCurrentSuitableChordsPitchClasses({});

      if (DEBUG_MODE.PIANO_ROLL) {
        console.log(`Chord guide disabled - cleared suitable chords`);
      }
    } else {
      // Get suitable chords for the selected function (T/S/D)
      const functionType = chordGuide as 'T' | 'S' | 'D';
      const suitableChords = getSuitableChords(keySignature, selectedMode, functionType);

      // Convert note names to pitch classes (ensuring ascending order)
      const chordsPitchClasses: Record<string, number[]> = {};
      for (const [chordSymbol, noteNames] of Object.entries(suitableChords)) {
        const pitchClasses: number[] = [];
        let previousPitch = -1;

        for (const noteName of noteNames) {
          let pitchClass = noteNameToPitchClass(noteName);

          // If this pitch is lower than the previous one, add an octave
          if (previousPitch >= 0 && pitchClass <= previousPitch) {
            pitchClass += 12;
          }

          pitchClasses.push(pitchClass);
          previousPitch = pitchClass;
        }

        chordsPitchClasses[chordSymbol] = pitchClasses;
      }

      // Update piano roll state
      pianoRollState.setCurrentSuitableChords(suitableChords);
      pianoRollState.setCurrentSuitableChordsPitchClasses(chordsPitchClasses);

      if (DEBUG_MODE.PIANO_ROLL) {
        console.log(`Chord guide updated: ${chordGuide} (${functionType})`);
        console.log(`Suitable chords for ${keySignature} in ${selectedMode} mode:`, suitableChords);
        console.log(`Pitch classes:`, chordsPitchClasses);
      }
    }
  }, [chordGuide, keySignature, selectedMode]);

  // Handler for receiving the setNoteUpdateCounter function from PianoRollContent
  const handleSetNoteUpdateTrigger = (setNoteFn: React.Dispatch<React.SetStateAction<number>>) => {
    triggerNoteUpdateRef.current = setNoteFn;
  };

  // Handler for receiving the deleteSelectedNotes function from PianoRollContent
  const handleSetDeleteNotesTrigger = (deleteFn: () => boolean) => {
    deleteSelectedNotesRef.current = deleteFn;
  };

  // Quantize selected notes based on the selected quantization value
  const quantizeSelectedNotes = useCallback((quantValue: string) => {
    if (!activeRegion) return;

    // Get the KGCore instance
    const core = KGCore.instance();

    // Get all selected notes
    const selectedItems = core.getSelectedItems();
    const selectedNotes = selectedItems.filter(item =>
      item instanceof KGMidiNote &&
      activeRegion.getNotes().some(note => note.getId() === item.getId())
    ) as KGMidiNote[];

    if (selectedNotes.length === 0) {
      if (DEBUG_MODE.PIANO_ROLL) {
        console.log('No notes selected for quantization');
      }
      return;
    }

    if (DEBUG_MODE.PIANO_ROLL) {
      console.log(`Quantizing ${selectedNotes.length} selected notes with value: ${quantValue}`);
    }

    // Parse the quantization value (e.g., "1/4", "1/8", "1/16", "1/32")
    const denominator = parseInt(quantValue.split('/')[1]);
    if (isNaN(denominator)) {
      console.error(`Invalid quantization value: ${quantValue}`);
      return;
    }

    // Calculate the quantization step in beats
    // In a 4/4 time signature, a quarter note (1/4) is 1 beat
    // In a 6/8 time signature, an eighth note (1/8) is 1 beat
    const { numerator, denominator: timeSigDenominator } = timeSignature;

    // Calculate beats per whole note based on time signature
    // In 4/4, a whole note is 4 beats
    // In 6/8, a whole note is 6 beats (because each beat is an eighth note)
    const beatsPerWholeNote = numerator * (4 / timeSigDenominator);

    // Calculate the quantization step in beats
    // quantizationStep should ALWAYS be 4 / denominator regardless of time signature
    const quantizationStep = 4 / denominator;

    if (DEBUG_MODE.PIANO_ROLL) {
      console.log(`Time signature: ${numerator}/${timeSigDenominator}`);
      console.log(`Beats per whole note: ${beatsPerWholeNote}`);
      console.log(`Quantization step: ${quantizationStep} beats`);
    }

    // Apply quantization to each selected note
    selectedNotes.forEach(note => {
      // Get the current start beat
      const currentStartBeat = note.getStartBeat();

      // Calculate the quantized start beat
      const quantizedStartBeat = Math.round(currentStartBeat / quantizationStep) * quantizationStep;

      // Calculate the duration of the note
      const duration = note.getEndBeat() - currentStartBeat;

      // Set the new start beat and maintain the duration
      note.setStartBeat(quantizedStartBeat);
      note.setEndBeat(quantizedStartBeat + duration);

      if (DEBUG_MODE.PIANO_ROLL) {
        console.log(`Quantized note ${note.getId()}: ${currentStartBeat} -> ${quantizedStartBeat}`);
      }
    });

    // Find the track that contains this region and update it
    const track = tracks.find(t => t.getId().toString() === activeRegion.getTrackId());
    if (track) {
      updateTrack(track);
    }

    // Trigger a re-render by incrementing the note update counter
    if (triggerNoteUpdateRef.current) {
      triggerNoteUpdateRef.current(prev => prev + 1);

      if (DEBUG_MODE.PIANO_ROLL) {
        console.log('Triggered note update to re-render quantized notes');
      }
    }
  }, [activeRegion, timeSignature, updateTrack, tracks]);

  // Quantize selected notes length based on the selected quantization value
  const quantizeNoteLength = useCallback((quantValue: string) => {
    if (!activeRegion) return;

    // Get the KGCore instance
    const core = KGCore.instance();

    // Get all selected notes
    const selectedItems = core.getSelectedItems();
    const selectedNotes = selectedItems.filter(item =>
      item instanceof KGMidiNote &&
      activeRegion.getNotes().some(note => note.getId() === item.getId())
    ) as KGMidiNote[];

    if (selectedNotes.length === 0) {
      if (DEBUG_MODE.PIANO_ROLL) {
        console.log('No notes selected for length quantization');
      }
      return;
    }

    if (DEBUG_MODE.PIANO_ROLL) {
      console.log(`Quantizing length of ${selectedNotes.length} selected notes with value: ${quantValue}`);
    }

    // Parse the quantization value (e.g., "1/1", "1/2", "1/4", "1/8", "1/16", "1/32")
    const denominator = parseInt(quantValue.split('/')[1]);
    if (isNaN(denominator)) {
      console.error(`Invalid quantization value: ${quantValue}`);
      return;
    }

    // Calculate the quantization step in beats
    // In a 4/4 time signature, a quarter note (1/4) is 1 beat
    // In a 6/8 time signature, an eighth note (1/8) is 1 beat
    const { numerator, denominator: timeSigDenominator } = timeSignature;

    // Calculate beats per whole note based on time signature
    // In 4/4, a whole note is 4 beats
    // In 6/8, a whole note is 6 beats (because each beat is an eighth note)
    const beatsPerWholeNote = numerator * (4 / timeSigDenominator);

    // Calculate the quantization step in beats
    // quantizationStep should ALWAYS be 4 / denominator regardless of time signature
    const quantizationStep = 4 / denominator;

    if (DEBUG_MODE.PIANO_ROLL) {
      console.log(`Time signature: ${numerator}/${timeSigDenominator}`);
      console.log(`Beats per whole note: ${beatsPerWholeNote}`);
      console.log(`Length quantization step: ${quantizationStep} beats`);
    }

    // Apply quantization to each selected note
    selectedNotes.forEach(note => {
      // Get the current start and end beats
      const startBeat = note.getStartBeat();
      const currentEndBeat = note.getEndBeat();

      // Calculate the current duration
      const currentDuration = currentEndBeat - startBeat;

      // Calculate the quantized duration
      // If the current duration is less than the quantization step,
      // extend it to match the quantization step exactly
      // Otherwise, round to the nearest multiple of quantizationStep
      let quantizedDuration;

      if (currentDuration < quantizationStep) {
        // For notes shorter than the quantization step, extend to exactly one step
        quantizedDuration = quantizationStep;

        if (DEBUG_MODE.PIANO_ROLL) {
          console.log(`Extending short note ${note.getId()} from ${currentDuration} to ${quantizedDuration}`);
        }
      } else {
        // For longer notes, round to nearest multiple of quantizationStep
        quantizedDuration = Math.round(currentDuration / quantizationStep) * quantizationStep;
      }

      // Ensure minimum note length
      quantizedDuration = Math.max(PIANO_ROLL_CONSTANTS.MIN_NOTE_LENGTH, quantizedDuration);

      // Set the new end beat while maintaining the start beat
      note.setEndBeat(startBeat + quantizedDuration);

      if (DEBUG_MODE.PIANO_ROLL) {
        console.log(`Quantized note length ${note.getId()}: ${currentDuration} -> ${quantizedDuration}`);
      }
    });

    // Find the track that contains this region and update it
    const track = tracks.find(t => t.getId().toString() === activeRegion.getTrackId());
    if (track) {
      updateTrack(track);
    }

    // Trigger a re-render by incrementing the note update counter
    if (triggerNoteUpdateRef.current) {
      triggerNoteUpdateRef.current(prev => prev + 1);

      if (DEBUG_MODE.PIANO_ROLL) {
        console.log('Triggered note update to re-render quantized note lengths');
      }
    }
  }, [activeRegion, timeSignature, updateTrack, tracks]);

  // Handle quantization selection
  const handleQuantSelect = useCallback((type: 'position' | 'length', value: string) => {
    if (type === 'position') {
      setQuantPosition(value);

      // Apply quantization immediately when position quantization is changed
      quantizeSelectedNotes(value);

      if (DEBUG_MODE.PIANO_ROLL) {
        console.log(`quant-position selected: ${value}`);
      }
    } else {
      setQuantLength(value);

      // Apply length quantization immediately when length quantization is changed
      quantizeNoteLength(value);

      if (DEBUG_MODE.PIANO_ROLL) {
        console.log(`quant-length selected: ${value}`);
      }
    }
  }, [quantizeSelectedNotes, quantizeNoteLength]);

  const handleZoomChange = useCallback((nextZoom: number) => {
    if (nextZoom === pianoRollZoom) return;

    const container = pianoRollNoteScrollRef.current;
    pendingZoomAnchorBeatRef.current = null;
    if (container) {
      const keysWidth = parseInt(
        getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
      ) || 60;
      const visibleMusicWidth = Math.max(0, container.clientWidth - keysWidth);
      const beatWidth = parseInt(
        getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
      ) || TOOLBAR_CONSTANTS.BASE_BAR_WIDTH;

      if (visibleMusicWidth > 0 && beatWidth > 0) {
        pendingZoomAnchorBeatRef.current = (container.scrollLeft + visibleMusicWidth / 2) / beatWidth;
      } else {
        pendingZoomAnchorBeatRef.current = null;
      }
    }

    KGPianoRollState.instance().setPianoRollZoom(nextZoom);
    KGCore.instance().getCurrentProject().setPianoRollZoom(nextZoom);
    setPianoRollZoom(nextZoom);
  }, [pianoRollZoom]);

  const handleAutomationToggle = useCallback(() => {
    setAutomationEnabled(current => {
      const next = !current;
      KGPianoRollState.instance().setAutomationViewEnabled(next);
      return next;
    });
  }, []);

  const handleAutomationTypeChange = useCallback((value: PianoRollAutomationType) => {
    setAutomationType(value);
    KGPianoRollState.instance().setCurrentAutomationType(value);
  }, []);

  const handleSheetMusicViewToggle = useCallback(() => {
    if (activeRegion) {
      pendingModeSwitchRequestRef.current = createPendingModeSwitchRequest({
        playheadBeat: playheadPosition,
        regionStartBeat: activeRegion.getStartFromBeat(),
        regionEndBeat: activeRegion.getStartFromBeat() + activeRegion.getLength(),
        sourceSheetMusicViewEnabled: sheetMusicViewEnabled,
        destinationSheetMusicViewEnabled: !sheetMusicViewEnabled,
        destinationSheetMusicTrackScopeEnabled: !sheetMusicViewEnabled && sheetMusicTrackScopeEnabled,
      });
    } else {
      pendingModeSwitchRequestRef.current = null;
    }

    setSheetMusicViewEnabled(current => {
      const next = !current;
      KGPianoRollState.instance().setSheetMusicViewEnabled(next);
      return next;
    });
  }, [activeRegion, playheadPosition, sheetMusicTrackScopeEnabled, sheetMusicViewEnabled]);

  const handleSheetQuantizationChange = useCallback((value: string) => {
    setSheetQuantization(value);
    KGPianoRollState.instance().setSheetQuantization(value);
  }, []);

  const handleSheetMusicTrackScopeToggle = useCallback(() => {
    if (activeRegion) {
      pendingModeSwitchRequestRef.current = createPendingModeSwitchRequest({
        playheadBeat: playheadPosition,
        regionStartBeat: activeRegion.getStartFromBeat(),
        regionEndBeat: activeRegion.getStartFromBeat() + activeRegion.getLength(),
        sourceSheetMusicViewEnabled: sheetMusicViewEnabled,
        destinationSheetMusicViewEnabled: sheetMusicViewEnabled,
        destinationSheetMusicTrackScopeEnabled: !sheetMusicTrackScopeEnabled,
      });
    } else {
      pendingModeSwitchRequestRef.current = null;
    }

    setSheetMusicTrackScopeEnabled(current => {
      const next = !current;
      KGPianoRollState.instance().setSheetMusicTrackScopeEnabled(next);
      return next;
    });
  }, [activeRegion, playheadPosition, sheetMusicTrackScopeEnabled, sheetMusicViewEnabled]);

  const handleSheetMeasureMetricsChange = useCallback((metrics: SheetMeasureMetric[]) => {
    setSheetMeasureMetrics((current) => {
      if (
        current.length === metrics.length &&
        current.every((metric, index) => (
          metric.barIndex === metrics[index].barIndex &&
          metric.startBeat === metrics[index].startBeat &&
          metric.endBeat === metrics[index].endBeat &&
          metric.leftPx === metrics[index].leftPx &&
          metric.widthPx === metrics[index].widthPx
        ))
      ) {
        return current;
      }

      return metrics;
    });
  }, []);

  const centerPianoRollOnDefaultVerticalPosition = useCallback(() => {
    const container = pianoRollNoteScrollRef.current;
    if (!container) {
      return;
    }

    const keyHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-height')) || 20;
    const c4Position = 4 * 12 * keyHeight;
    const totalHeight = 8 * 12 * keyHeight;
    const viewportHeight = container.clientHeight;
    const scrollPosition = (totalHeight - c4Position) - (viewportHeight / 2);

    container.scrollTop = Math.max(0, scrollPosition);
  }, []);

  // Calculate C4 position and scroll to it when piano roll opens
  useEffect(() => {
    centerPianoRollOnDefaultVerticalPosition();
  }, [centerPianoRollOnDefaultVerticalPosition]);

  useEffect(() => {
    const previous = previousSheetMusicViewEnabledRef.current;

    if (previous !== sheetMusicViewEnabled && !sheetMusicViewEnabled) {
      centerPianoRollOnDefaultVerticalPosition();
    }

    previousSheetMusicViewEnabledRef.current = sheetMusicViewEnabled;
  }, [centerPianoRollOnDefaultVerticalPosition, sheetMusicViewEnabled]);

  // Sync isPlayingRef for use inside scroll event closure
  useEffect(() => {
    pianoRollIsPlayingRef.current = isPlaying;
  }, [isPlaying]);

  // Detect manual horizontal scroll during playback
  useEffect(() => {
    const container = pianoRollNoteScrollRef.current;
    if (!container) return;

    const handleScroll = () => {
      if (!pianoRollIsPlayingRef.current) return;
      if (Math.abs(container.scrollLeft - pianoRollExpectedScrollLeftRef.current) < 1) return;
      useProjectStore.getState().setAutoScrollEnabled(false);
    };

    container.addEventListener('scroll', handleScroll);
    return () => {
      container.removeEventListener('scroll', handleScroll);
    };
  }, []);

  // Auto-scroll to keep playhead centered during playback
  useEffect(() => {
    if (!isPlaying || !autoScrollEnabled) return;

    const container = pianoRollNoteScrollRef.current;
    if (!container) return;

    const playheadPixel = sheetMusicViewEnabled && activeRegion
      ? getSheetPlayheadPixel(
          sheetMusicTrackScopeEnabled
            ? Math.max(0, playheadPosition)
            : Math.max(0, playheadPosition - activeRegion.getStartFromBeat()),
          sheetMeasureMetrics
        )
      : (() => {
          const beatWidth = parseInt(
            getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
          ) || 40;
          return playheadPosition * beatWidth;
        })();

    // Center the playhead in the visible grid area (excluding the 60px sticky piano keys panel)
    const keysWidth = sheetMusicViewEnabled
      ? 0
      : (parseInt(
          getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
        ) || 60);
    const targetScrollLeft = playheadPixel - (container.clientWidth - keysWidth) / 2;
    const clampedScrollLeft = Math.max(
      0,
      Math.min(targetScrollLeft, container.scrollWidth - container.clientWidth)
    );

    pianoRollExpectedScrollLeftRef.current = clampedScrollLeft;
    container.scrollLeft = clampedScrollLeft;
  }, [playheadPosition, isPlaying, autoScrollEnabled, sheetMusicViewEnabled, sheetMusicTrackScopeEnabled, sheetMeasureMetrics, activeRegion]);

  // Handle scroll requests from main content bar numbers clicks
  useEffect(() => {
    if (pianoRollScrollRequest === null) return;

    const container = pianoRollNoteScrollRef.current;
    if (!container) return;

    const playheadPixel = sheetMusicViewEnabled && activeRegion
      ? getSheetPlayheadPixel(
          sheetMusicTrackScopeEnabled
            ? Math.max(0, pianoRollScrollRequest)
            : Math.max(0, pianoRollScrollRequest - activeRegion.getStartFromBeat()),
          sheetMeasureMetrics
        )
      : (() => {
          const beatWidth = parseInt(
            getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
          ) || 40;
          return pianoRollScrollRequest * beatWidth;
        })();

    // Center the playhead in the visible grid area (excluding the 60px sticky piano keys panel)
    const keysWidth = sheetMusicViewEnabled
      ? 0
      : (parseInt(
          getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
        ) || 60);
    const targetScrollLeft = playheadPixel - (container.clientWidth - keysWidth) / 2;
    const clampedScrollLeft = Math.max(
      0,
      Math.min(targetScrollLeft, container.scrollWidth - container.clientWidth)
    );

    container.scrollLeft = clampedScrollLeft;

    // Clear the request after handling
    useProjectStore.setState({ pianoRollScrollRequest: null });
  }, [pianoRollScrollRequest, sheetMusicViewEnabled, sheetMusicTrackScopeEnabled, sheetMeasureMetrics, activeRegion]);

  // Update --region-grid-beat-width when zoom changes and preserve the centered beat position.
  useLayoutEffect(() => {
    const beatWidth = TOOLBAR_CONSTANTS.BASE_BAR_WIDTH * pianoRollZoom;
    document.documentElement.style.setProperty('--region-grid-beat-width', `${beatWidth}px`);
    if (triggerNoteUpdateRef.current) {
      triggerNoteUpdateRef.current(prev => prev + 1);
    }

    const anchorBeat = pendingZoomAnchorBeatRef.current;
    const container = pianoRollNoteScrollRef.current;
    if (anchorBeat !== null && container) {
      const keysWidth = parseInt(
        getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
      ) || 60;
      const visibleMusicWidth = Math.max(0, container.clientWidth - keysWidth);
      const targetPixel = anchorBeat * beatWidth;
      const targetScrollLeft = targetPixel - visibleMusicWidth / 2;
      const clampedScrollLeft = Math.max(
        0,
        Math.min(targetScrollLeft, container.scrollWidth - container.clientWidth)
      );

      pianoRollExpectedScrollLeftRef.current = clampedScrollLeft;
      container.scrollLeft = clampedScrollLeft;
      pendingZoomAnchorBeatRef.current = null;
    }

    return () => {
      document.documentElement.style.setProperty('--region-grid-beat-width', '40px');
    };
  }, [pianoRollZoom]);

  // Scroll horizontally to the active region's starting position
  useEffect(() => {
    if (!pianoRollNoteScrollRef.current || !activeRegion) {
      previousActiveRegionIdRef.current = activeRegion?.getId() ?? null;
      return;
    }

    if (pendingModeSwitchRequestRef.current !== null) {
      previousActiveRegionIdRef.current = activeRegion.getId();
      return;
    }

    if (previousActiveRegionIdRef.current !== activeRegion.getId()) {
      // Get the starting beat of the region
      const startBeat = activeRegion.getStartFromBeat();

      if (DEBUG_MODE.PIANO_ROLL) {
        console.log(`Scrolling to region's starting position: startBeat=${startBeat}`);
      }

      const scrollPosition = getRegionStartScrollLeft(startBeat);

      // Scroll to the calculated position
      pianoRollNoteScrollRef.current.scrollLeft = Math.max(0, scrollPosition);
      previousActiveRegionIdRef.current = activeRegion.getId();
    }
  }, [activeRegion]);

  useLayoutEffect(() => {
    const request = pendingModeSwitchRequestRef.current;
    const container = pianoRollNoteScrollRef.current;
    if (!request || !container || !activeRegion) {
      return;
    }

    if (request.destinationSheetMusicViewEnabled && sheetMeasureMetrics.length === 0) {
      return;
    }

    const targetScrollLeft = getScrollLeftForViewportRequest({
      request,
      container,
      sheetMeasureMetrics,
      activeRegionStartBeat: activeRegion.getStartFromBeat(),
      activeRegionEndBeat: activeRegion.getStartFromBeat() + activeRegion.getLength(),
      songEndBeat: maxBars * timeSignature.numerator,
    });

    pianoRollExpectedScrollLeftRef.current = targetScrollLeft;
    container.scrollLeft = targetScrollLeft;
    pendingModeSwitchRequestRef.current = null;
  }, [activeRegion, maxBars, sheetMeasureMetrics, sheetMusicTrackScopeEnabled, sheetMusicViewEnabled, timeSignature.numerator]);

  // Add keyboard event listener for piano roll hotkeys (snapping and quantization)
  useEffect(() => {
    const handlePianoRollKeyDown = (event: KeyboardEvent) => {
      // Skip if user is typing in an input field (including ChatBox)
      const target = event.target as HTMLElement;
      if (target && (
        target.tagName === 'INPUT' ||
        target.tagName === 'TEXTAREA' ||
        target.contentEditable === 'true' ||
        target.hasAttribute('data-chatbox-input') ||
        target.closest('.chatbox-input')
      )) {
        return;
      }

      // Handle delete key for selected notes
      if (event.key === 'Backspace' || event.key === 'Delete') {
        if (deleteSelectedNotesRef.current) {
          const deleted = deleteSelectedNotesRef.current();
          if (deleted) {
            // Prevent default behavior only if notes were actually deleted
            event.preventDefault();
          }
        }
        return;
      }

      // Handle piano roll hotkeys
      const configManager = ConfigManager.instance();
      if (configManager.getIsInitialized()) {
        // Chord guide switch hotkey
        const switch_key = configManager.get('hotkeys.piano_roll.switch') as string;
        if (event.key && event.key.toLowerCase() === switch_key.toLowerCase()) {
          // Call the switchChord function exposed by PianoGrid
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          const switchChord = (window as any).__pianoGridSwitchChord;
          if (typeof switchChord === 'function') {
            switchChord();
            event.preventDefault();
          }
          return;
        }

        // Snapping hotkeys
        const snap_none_key = configManager.get('hotkeys.piano_roll.snap_none') as string;
        const snap_1_4_key = configManager.get('hotkeys.piano_roll.snap_1_4') as string;
        const snap_1_8_key = configManager.get('hotkeys.piano_roll.snap_1_8') as string;
        const snap_1_16_key = configManager.get('hotkeys.piano_roll.snap_1_16') as string;

        // Quantize position hotkeys
        const qua_pos_1_4_key = configManager.get('hotkeys.piano_roll.qua_pos_1_4') as string;
        const qua_pos_1_8_key = configManager.get('hotkeys.piano_roll.qua_pos_1_8') as string;
        const qua_pos_1_16_key = configManager.get('hotkeys.piano_roll.qua_pos_1_16') as string;

        // Quantize length hotkeys
        const qua_len_1_4_key = configManager.get('hotkeys.piano_roll.qua_len_1_4') as string;
        const qua_len_1_8_key = configManager.get('hotkeys.piano_roll.qua_len_1_8') as string;
        const qua_len_1_16_key = configManager.get('hotkeys.piano_roll.qua_len_1_16') as string;

        let actionType: 'snap' | 'quantize' | null = null;
        let actionValue: string | null = null;
        let quantType: 'position' | 'length' | null = null;

        // Check snapping hotkeys
        if (event.key === snap_none_key) {
          actionType = 'snap';
          actionValue = 'NO SNAP';
        } else if (event.key === snap_1_4_key) {
          actionType = 'snap';
          actionValue = '1/4';
        } else if (event.key === snap_1_8_key) {
          actionType = 'snap';
          actionValue = '1/8';
        } else if (event.key === snap_1_16_key) {
          actionType = 'snap';
          actionValue = '1/16';
        }
        // Check quantize position hotkeys
        else if (event.key === qua_pos_1_4_key) {
          actionType = 'quantize';
          actionValue = '1/4';
          quantType = 'position';
        } else if (event.key === qua_pos_1_8_key) {
          actionType = 'quantize';
          actionValue = '1/8';
          quantType = 'position';
        } else if (event.key === qua_pos_1_16_key) {
          actionType = 'quantize';
          actionValue = '1/16';
          quantType = 'position';
        }
        // Check quantize length hotkeys
        else if (event.key === qua_len_1_4_key) {
          actionType = 'quantize';
          actionValue = '1/4';
          quantType = 'length';
        } else if (event.key === qua_len_1_8_key) {
          actionType = 'quantize';
          actionValue = '1/8';
          quantType = 'length';
        } else if (event.key === qua_len_1_16_key) {
          actionType = 'quantize';
          actionValue = '1/16';
          quantType = 'length';
        }

        if (actionType && actionValue) {
          // Prevent default behavior
          event.preventDefault();

          if (actionType === 'snap') {
            // Validate the snap value exists in snap options
            if (KGPianoRollState.SNAP_OPTIONS.includes(actionValue)) {
              // Change snapping value
              handleSnappingSelect(actionValue);

              // Trigger blink effect for visual feedback
              setBlinkButton('snapping');
              setTimeout(() => setBlinkButton(null), 200);

              if (DEBUG_MODE.PIANO_ROLL) {
                console.log(`Snap hotkey triggered: ${event.key} → ${actionValue}`);
              }
            }
          } else if (actionType === 'quantize' && quantType) {
            // Validate the quantValue exists in the appropriate options
            const validOptions = quantType === 'length' ? KGPianoRollState.QUANT_LEN_OPTIONS : KGPianoRollState.QUANT_POS_OPTIONS;

            if (validOptions.includes(actionValue)) {
              // Apply quantization
              handleQuantSelect(quantType, actionValue);

              // Trigger blink effect for visual feedback
              const buttonName = quantType === 'length' ? 'quant-length' : 'quant-position';
              setBlinkButton(buttonName);
              setTimeout(() => setBlinkButton(null), 200);

              if (DEBUG_MODE.PIANO_ROLL) {
                console.log(`Quantize ${quantType} hotkey triggered: ${event.key} → ${actionValue}`);
              }
            }
          }
        }
      }
    };

    // Add event listener
    window.addEventListener('keydown', handlePianoRollKeyDown);

    // Remove event listener on cleanup
    return () => {
      window.removeEventListener('keydown', handlePianoRollKeyDown);
    };
  }, [handleQuantSelect, handleSnappingSelect]);

  // Get the title for the piano roll based on the active region
  const getPianoRollTitle = () => {
    if (isSpectrogram) return audioRegion ? `SPECTROGRAM — ${audioRegion.getName()}` : 'SPECTROGRAM';
    if (isHybrid) {
      const midiName = activeRegion?.getName() ?? 'MIDI';
      const audioName = audioRegion?.getName() ?? 'Audio';
      return `${midiName} + ${audioName}`;
    }
    if (!activeRegion) return "EDIT NOTE CLIP";

    // Calculate the bar and beat position of the region
    const startBeat = activeRegion.getStartFromBeat();
    const { bar, beatInBar } = beatsToBar(startBeat, timeSignature);

    // Format as 1-indexed bar and beat (bar + 1, beatInBar + 1)
    const barNumber = bar + 1;
    const beatNumber = beatInBar + 1;

    return `${activeRegion.getName()} (at ${barNumber}:${beatNumber})`;
  };

  return (
    <div
      className="piano-roll-panel"
      style={{
        position: 'fixed',
        left: `${position.x}px`,
        top: `${position.y}px`,
        width: `${size.width}px`,
        height: `${size.height}px`,
        zIndex: 2000
      }}
      ref={pianoRollRef}
    >
      <PianoRollHeader
        onClose={onClose}
        title={getPianoRollTitle()}
        onTitleClick={handleTitleClick}
        onMouseDown={(e) => handleMouseDown(e, 'drag')}
      />

      <PianoRollToolbar
        sheetMusicViewEnabled={sheetMusicViewEnabled}
        onSheetMusicViewToggle={handleSheetMusicViewToggle}
        sheetMusicTrackScopeEnabled={sheetMusicTrackScopeEnabled}
        onSheetMusicTrackScopeToggle={handleSheetMusicTrackScopeToggle}
        sheetQuantization={sheetQuantization}
        onSheetQuantizationChange={handleSheetQuantizationChange}
        sheetQuantizationOptions={getSheetQuantizationOptions()}
        activeTool={activeTool}
        onToolSelect={handleToolSelect}
        quantPosition={quantPosition}
        quantLength={quantLength}
        onQuantSelect={handleQuantSelect}
        snapping={snapping}
        onSnappingSelect={handleSnappingSelect}
        selectedMode={selectedMode}
        onModeChange={handleModeSelect}
        chordGuide={chordGuide}
        onChordGuideChange={handleChordGuideSelect}
        blinkButton={blinkButton}
        mode={mode}
        thresholdDb={spectrogramThresholdDb}
        onThresholdChange={setSpectrogramThresholdDb}
        power={spectrogramPower}
        onPowerChange={setSpectrogramPower}
        zoom={pianoRollZoom}
        onZoomChange={handleZoomChange}
        showAutomationControls={!isSpectrogram}
        automationEnabled={automationEnabled}
        automationType={automationType}
        onAutomationToggle={handleAutomationToggle}
        onAutomationTypeChange={handleAutomationTypeChange}
      />

      <NoteAttributeBar selectedNotes={selectedNotes} isSpectrogram={isSpectrogram} activeRegion={activeRegion} />

      <PianoRollContent
        contentRef={pianoRollContentRef}
        noteScrollRef={pianoRollNoteScrollRef}
        pianoGridRef={pianoGridRef}
        maxBars={maxBars}
        timeSignature={timeSignature}
        activeRegion={activeRegion}
        updateTrack={updateTrack}
        tracks={tracks}
        onSetNoteUpdateTrigger={handleSetNoteUpdateTrigger}
        onSetDeleteNotesTrigger={handleSetDeleteNotesTrigger}
        selectedMode={selectedMode}
        keySignature={keySignature}
        chordGuide={chordGuide}
        mode={mode}
        audioRegion={audioRegion}
        trackId={trackId}
        projectName={projectName}
        bpm={bpm}
        spectrogramThresholdDb={spectrogramThresholdDb}
        spectrogramPower={spectrogramPower}
        spectrogramHeightResolution={spectrogramHeightResolution}
        pianoRollZoom={pianoRollZoom}
        automationEnabled={automationEnabled}
        automationType={automationType}
        automationRedrawVersion={automationRedrawVersion}
        sheetMusicViewEnabled={sheetMusicViewEnabled}
        sheetMusicTrackScopeEnabled={sheetMusicTrackScopeEnabled}
        sheetQuantization={parsedSheetQuantization}
        sheetKeySignature={keySignature}
        sheetInstrument={activeInstrument}
        onSheetMeasureMetricsChange={handleSheetMeasureMetricsChange}
      />

      <div
        className="resize-handle"
        onMouseDown={(e) => handleMouseDown(e, 'resize')}
      >
        <FaGripLines />
      </div>
    </div>
  );
};

export default PianoRoll; 

export function getRegionPlayheadRelation(
  playheadBeat: number,
  regionStartBeat: number,
  regionEndBeat: number
): RegionPlayheadRelation {
  if (playheadBeat < regionStartBeat) {
    return 'before';
  }

  if (playheadBeat > regionEndBeat) {
    return 'after';
  }

  return 'inside';
}

export function createPendingModeSwitchRequest({
  playheadBeat,
  regionStartBeat,
  regionEndBeat,
  sourceSheetMusicViewEnabled,
  destinationSheetMusicViewEnabled,
  destinationSheetMusicTrackScopeEnabled,
}: ModeSwitchRequestOptions): PendingModeSwitchRequest {
  const relation = getRegionPlayheadRelation(playheadBeat, regionStartBeat, regionEndBeat);
  const enteringTrackScopeSheet = (
    !sourceSheetMusicViewEnabled &&
    destinationSheetMusicViewEnabled &&
    destinationSheetMusicTrackScopeEnabled
  );

  if (relation === 'inside') {
    return {
      sourceSheetMusicViewEnabled,
      destinationSheetMusicViewEnabled,
      destinationSheetMusicTrackScopeEnabled,
      alignment: 'center',
      anchorBeat: playheadBeat,
      clampScope: destinationSheetMusicTrackScopeEnabled ? 'track' : 'region',
    };
  }

  if (enteringTrackScopeSheet) {
    return {
      sourceSheetMusicViewEnabled,
      destinationSheetMusicViewEnabled,
      destinationSheetMusicTrackScopeEnabled,
      alignment: 'center',
      anchorBeat: playheadBeat,
      clampScope: 'track',
    };
  }

  return {
    sourceSheetMusicViewEnabled,
    destinationSheetMusicViewEnabled,
    destinationSheetMusicTrackScopeEnabled,
    alignment: relation === 'before' ? 'region-start' : 'region-end',
    anchorBeat: relation === 'before' ? regionStartBeat : regionEndBeat,
    clampScope: 'region',
  };
}

function getHorizontalViewportMetrics(container: HTMLDivElement, sheetMusicViewEnabled: boolean): {
  visibleWidth: number;
  keysWidth: number;
} {
  const keysWidth = sheetMusicViewEnabled
    ? 0
    : (parseInt(
        getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
      ) || 60);

  return {
    visibleWidth: Math.max(0, container.clientWidth - keysWidth),
    keysWidth,
  };
}

function getPixelForAbsoluteBeat(
  beat: number,
  sheetMusicViewEnabled: boolean,
  sheetMusicTrackScopeEnabled: boolean,
  sheetMeasureMetrics: SheetMeasureMetric[],
  activeRegionStartBeat: number
): number {
  if (sheetMusicViewEnabled) {
    return getSheetPlayheadPixel(
      sheetMusicTrackScopeEnabled
        ? Math.max(0, beat)
        : Math.max(0, beat - activeRegionStartBeat),
      sheetMeasureMetrics
    );
  }

  const beatWidth = parseInt(
    getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
  ) || 40;
  return beat * beatWidth;
}

function getScopeBoundsInPixels(
  request: PendingModeSwitchRequest,
  sheetMeasureMetrics: SheetMeasureMetric[],
  activeRegionStartBeat: number,
  activeRegionEndBeat: number,
  songEndBeat: number
): { startPx: number; endPx: number } {
  if (request.destinationSheetMusicViewEnabled) {
    if (request.clampScope === 'track') {
      return {
        startPx: getSheetPlayheadPixel(0, sheetMeasureMetrics),
        endPx: getSheetPlayheadPixel(songEndBeat, sheetMeasureMetrics),
      };
    }

    return {
      startPx: getSheetPlayheadPixel(0, sheetMeasureMetrics),
      endPx: getSheetPlayheadPixel(activeRegionEndBeat - activeRegionStartBeat, sheetMeasureMetrics),
    };
  }

  const beatWidth = parseInt(
    getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
  ) || 40;

  if (request.clampScope === 'track') {
    return {
      startPx: 0,
      endPx: songEndBeat * beatWidth,
    };
  }

  return {
    startPx: activeRegionStartBeat * beatWidth,
    endPx: activeRegionEndBeat * beatWidth,
  };
}

function clampScrollLeftToContainer(container: HTMLDivElement, scrollLeft: number): number {
  return Math.max(0, Math.min(scrollLeft, container.scrollWidth - container.clientWidth));
}

function getCenteredScrollLeft({
  pixelPosition,
  visibleWidth,
  scopeStartPx,
  scopeEndPx,
  container,
}: {
  pixelPosition: number;
  visibleWidth: number;
  scopeStartPx: number;
  scopeEndPx: number;
  container: HTMLDivElement;
}): number {
  const unclamped = pixelPosition - visibleWidth / 2;
  const maxScopeScrollLeft = Math.max(scopeStartPx, scopeEndPx - visibleWidth);
  const clampedToScope = Math.max(scopeStartPx, Math.min(unclamped, maxScopeScrollLeft));
  return clampScrollLeftToContainer(container, clampedToScope);
}

function getRegionEndAlignedScrollLeft({
  visibleWidth,
  scopeEndPx,
  container,
}: {
  visibleWidth: number;
  scopeEndPx: number;
  container: HTMLDivElement;
}): number {
  return clampScrollLeftToContainer(container, Math.max(0, scopeEndPx - visibleWidth));
}

export function getScrollLeftForViewportRequest({
  request,
  container,
  sheetMeasureMetrics,
  activeRegionStartBeat,
  activeRegionEndBeat,
  songEndBeat,
}: ScrollLeftForViewportRequestOptions): number {
  const { visibleWidth } = getHorizontalViewportMetrics(
    container,
    request.destinationSheetMusicViewEnabled
  );
  const pixelPosition = getPixelForAbsoluteBeat(
    request.anchorBeat,
    request.destinationSheetMusicViewEnabled,
    request.destinationSheetMusicTrackScopeEnabled,
    sheetMeasureMetrics,
    activeRegionStartBeat
  );
  const { startPx, endPx } = getScopeBoundsInPixels(
    request,
    sheetMeasureMetrics,
    activeRegionStartBeat,
    activeRegionEndBeat,
    songEndBeat
  );

  if (request.alignment === 'region-start') {
    return clampScrollLeftToContainer(container, startPx);
  }

  if (request.alignment === 'region-end') {
    return getRegionEndAlignedScrollLeft({
      visibleWidth,
      scopeEndPx: endPx,
      container,
    });
  }

  return getCenteredScrollLeft({
    pixelPosition,
    visibleWidth,
    scopeStartPx: startPx,
    scopeEndPx: endPx,
    container,
  });
}

export function getRegionStartScrollLeft(startBeat: number): number {
  const beatWidth = parseInt(
    getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
  ) || TOOLBAR_CONSTANTS.BASE_BAR_WIDTH;

  return Math.max(0, startBeat * beatWidth);
}
