import type { Selectable } from '../components/interfaces';
import { KGProject } from './KGProject';
import { KGAudioInterface } from './audio-interface/KGAudioInterface';
import { ConfigManager } from './config/ConfigManager';
import { KGProjectStorage } from './io/KGProjectStorage';
import { KGConfigUpgrader } from './config-upgrader/KGConfigUpgrader';
import { KGMidiRegion } from './region/KGMidiRegion';
import { KGMidiNote } from './midi/KGMidiNote';
import { KGMidiControllerEvent } from './midi/KGMidiControllerEvent';
import { KGMidiPitchBend } from './midi/KGMidiPitchBend';
import { KGRegion } from './region/KGRegion';
import { generateUniqueId } from '../util/miscUtil';
import { KGCommand, KGCommandHistory } from './commands';

interface PlaybackStartOptions {
  preserveLoopPreroll?: boolean;
}

/**
 * KGCore - Main application class for the DAW
 * Implements the singleton pattern for global access
 */
export class KGCore {
  // Private static instance for singleton pattern
  private static _instance: KGCore | null = null;

  // Global music data resources
  public static ORIGINAL_FUNCTIONAL_CHORDS_DATA: Record<string, { name: string; steps: number[]; T: string[]; S: string[]; D: string[]; chords: Record<string, string[]> }> = {}; // Original functional chords loaded from functional_chords.json
  public static FUNCTIONAL_CHORDS_DATA: Record<string, { name: string; steps: number[]; T: string[]; S: string[]; D: string[]; chords: Record<string, string[]> }> = {}; // Active functional chords (either original or custom from user settings)

  private currentProject: KGProject = new KGProject();

  private status: string = "Ready";

  private playheadPosition: number = 0;  // in beats

  private selectedItems: Selectable[] = [];
  private copiedItems: Selectable[] = [];

  private isPlaying: boolean = false;
  private isMigrating: boolean = false;
  private migrationStateChangeCallback: ((isMigrating: boolean) => void) | null = null;

  // Timer management for playback
  private playbackIntervalId: number | null = null;
  private playbackStartTime: number = 0;
  private playbackStartPosition: number = 0;
  
  // Callback for external state updates (e.g., store)
  private playheadUpdateCallback: ((position: number) => void) | null = null;
  private playbackStateChangeCallback: ((isPlaying: boolean) => void) | null = null;
  private loopBoundaryReachedCallback: ((loopEndBeat: number) => void) | null = null;

  // Selection change callbacks for store synchronization
  private selectionChangeCallbacks: (() => void)[] = [];

  // Command history for undo/redo functionality
  private commandHistory: KGCommandHistory = KGCommandHistory.instance();

  // Private constructor to prevent direct instantiation
  private constructor() {
    console.log("KGCore initialized");
    // Initialize core components here
  }
  
  /**
   * Get the singleton instance of KGCore
   * Creates the instance if it doesn't exist yet
   */
  public static instance(): KGCore {
    if (!KGCore._instance) {
      KGCore._instance = new KGCore();
    }
    return KGCore._instance;
  }
  
  /**
   * Initialize the audio engine and core components
   */
  public async initialize(): Promise<void> {
    try {
      // Initialize configuration manager
      const configManager = ConfigManager.instance();
      await configManager.initialize();

      // Initialize OPFS project storage
      const projectStorage = KGProjectStorage.getInstance();
      await projectStorage.initialize();

      // Run app-level migrations (e.g., IndexedDB -> OPFS project migration)
      this.setMigrating(true);
      try {
        await KGConfigUpgrader.upgradeToLatest();
      } finally {
        this.setMigrating(false);
      }

      // Initialize audio interface
      const audioInterface = KGAudioInterface.instance();
      await audioInterface.initialize();

      console.log("KGCore components initialized successfully");
    } catch (error) {
      console.error("Failed to initialize KGCore:", error);
      throw error;
    }
  }
  
  /**
   * Clean up resources when application is closed
   */
  public async dispose(): Promise<void> {
    try {
      // Stop playback if playing
      if (this.isPlaying) {
        await this.stopPlaying();
      }

      // Dispose audio interface
      const audioInterface = KGAudioInterface.instance();
      await audioInterface.dispose();

      // Dispose MIDI input (dynamic import to avoid circular dependency)
      const { KGMidiInput } = await import('./midi-input/KGMidiInput');
      const midiInput = KGMidiInput.instance();
      await midiInput.dispose();

      // Dispose config manager
      const configManager = ConfigManager.instance();
      await configManager.dispose();

      // Clear playback timer
      this.stopPlaybackUpdates();

      console.log("KGCore resources disposed successfully");
    } catch (error) {
      console.error("Error disposing KGCore resources:", error);
    }
  }
  
  // Add more core functionality methods here
  public getCurrentProject(): KGProject {
    return this.currentProject;
  }

  public setCurrentProject(project: KGProject): void {
    this.currentProject = project;
    
    // Sync project settings with audio interface
    try {
      const audioInterface = KGAudioInterface.instance();
      if (audioInterface.getIsInitialized()) {
        audioInterface.setBpm(project.getBpm());
      }
    } catch (error) {
      console.error('Error syncing project with audio interface:', error);
    }
  }

  public setStatus(status: string): void {
    this.status = status;
  }

  public getStatus(): string {
    return this.status;
  }

  public getPlayheadPosition(): number {
    return this.playheadPosition;
  }

  public setPlayheadPosition(position: number): void {
    this.playheadPosition = position;
    
    // Sync with audio interface transport if not playing
    // (During playback, audio interface controls transport position)
    if (!this.isPlaying) {
      try {
        const audioInterface = KGAudioInterface.instance();
        if (audioInterface.getIsInitialized()) {
          audioInterface.setTransportPosition(position);
        }
      } catch (error) {
        console.error('Error syncing playhead position with audio interface:', error);
      }
    }
    
    // Notify external listeners if callback is set
    if (this.playheadUpdateCallback) {
      this.playheadUpdateCallback(position);
    }
  }

  // Method to set external update callback
  public setPlayheadUpdateCallback(callback: (position: number) => void): void {
    this.playheadUpdateCallback = callback;
  }

  // Method to set external playback state change callback
  public setPlaybackStateChangeCallback(callback: (isPlaying: boolean) => void): void {
    this.playbackStateChangeCallback = callback;
  }

  public setLoopBoundaryReachedCallback(callback: ((loopEndBeat: number) => void) | null): void {
    this.loopBoundaryReachedCallback = callback;
  }

  // Selection change callback management
  public onSelectionChanged(callback: () => void): void {
    this.selectionChangeCallbacks.push(callback);
  }

  public removeSelectionChangeCallback(callback: () => void): void {
    const index = this.selectionChangeCallbacks.indexOf(callback);
    if (index > -1) {
      this.selectionChangeCallbacks.splice(index, 1);
    }
  }

  private notifySelectionChanged(): void {
    this.selectionChangeCallbacks.forEach(callback => callback());
  }

  // Migration state
  public getIsMigrating(): boolean {
    return this.isMigrating;
  }

  private setMigrating(value: boolean): void {
    this.isMigrating = value;
    if (this.migrationStateChangeCallback) {
      this.migrationStateChangeCallback(value);
    }
  }

  public setMigrationStateChangeCallback(callback: (isMigrating: boolean) => void): void {
    this.migrationStateChangeCallback = callback;
  }

  // play
  public getIsPlaying(): boolean {
    return this.isPlaying;
  }

  public async preparePlay(options?: PlaybackStartOptions): Promise<void> {
    try {
      const audioInterface = KGAudioInterface.instance();
      
      // Ensure audio context is started (required for Web Audio)
      await audioInterface.startAudioContext();
      
      // Prepare playback with current project and playhead position
      audioInterface.preparePlayback(this.currentProject, this.playheadPosition, {
        allowStartBeforeLoopStart: options?.preserveLoopPreroll ?? false,
      });
      
      // Sync BPM and transport settings
      audioInterface.setBpm(this.currentProject.getBpm());
      audioInterface.setTransportPosition(this.playheadPosition);
      
      console.log("Playback prepared successfully");
    } catch (error) {
      console.error("Failed to prepare playback:", error);
      throw error;
    }
  }

  public async play(): Promise<void> {
    try {
      const audioInterface = KGAudioInterface.instance();
      
      // Start audio playback
      audioInterface.startPlayback();
      
      // Update local state
      this.isPlaying = true;
      if (this.playbackStateChangeCallback) {
        this.playbackStateChangeCallback(true);
      }
      
      console.log("Audio playback started successfully");
    } catch (error) {
      console.error("Failed to start playback:", error);
      this.isPlaying = false;
      if (this.playbackStateChangeCallback) {
        this.playbackStateChangeCallback(false);
      }
      throw error;
    }
  }

  public async stop(): Promise<void> {
    try {
      const audioInterface = KGAudioInterface.instance();
      
      // Stop audio playback
      audioInterface.stopPlayback();
      
      // Update local state
      this.isPlaying = false;
      if (this.playbackStateChangeCallback) {
        this.playbackStateChangeCallback(false);
      }
      
      console.log("Audio playback stopped successfully");
    } catch (error) {
      console.error("Failed to stop playback:", error);
      // Still update state even if stop fails
      this.isPlaying = false;
      if (this.playbackStateChangeCallback) {
        this.playbackStateChangeCallback(false);
      }
    }
  }

  // High-level playback control methods
  public async startPlaying(options?: PlaybackStartOptions): Promise<void> {
    // Handle loop mode initialization
    if (this.currentProject.getIsLooping()) {
      const [startBar, endBar] = this.currentProject.getLoopingRange();

      // Handle [0, 0] case - set to full project
      if (startBar === 0 && endBar === 0) {
        const maxBars = this.currentProject.getMaxBars();
        const { ChangeLoopSettingsCommand } = await import('./commands');
        this.executeCommand(new ChangeLoopSettingsCommand({
          loopingRange: [0, maxBars]
        }));
      }

      // Move playhead to loop start (use updated range if [0,0] was just set)
      const updatedRange = this.currentProject.getLoopingRange();
      const beatsPerBar = this.currentProject.getTimeSignature().numerator;
      const loopStartBeats = updatedRange[0] * beatsPerBar;
      if (!options?.preserveLoopPreroll) {
        this.setPlayheadPosition(loopStartBeats);
      }
    }

    // Prepare playback first
    await this.preparePlay(options);

    // Start playing (non-blocking)
    this.play(); // Don't await this

    // Set up the regular playback update timer
    this.playbackStartTime = performance.now();
    this.playbackStartPosition = this.playheadPosition;

    this.startPlaybackUpdates();

  }

  public async stopPlaying(): Promise<void> {
    // Stop the timer first
    this.stopPlaybackUpdates();
    
    // Stop playback (wait for completion)
    await this.stop();
  }

  // Timer management for regular playback updates
  private startPlaybackUpdates(): void {
    if (this.playbackIntervalId !== null) {
      this.stopPlaybackUpdates(); // Clear any existing timer
    }

    // Get playhead update frequency from config (in fps)
    const configManager = ConfigManager.instance();
    const updateFrequency = (configManager.get('editor.playhead_update_frequency') as number) ?? 30;
    const updateIntervalMs = 1000 / updateFrequency; // Convert fps to milliseconds

    this.playbackIntervalId = window.setInterval(() => {
      this.onPlaybackUpdate();
    }, updateIntervalMs);
  }

  private stopPlaybackUpdates(): void {
    if (this.playbackIntervalId !== null) {
      clearInterval(this.playbackIntervalId);
      this.playbackIntervalId = null;
    }
  }

  // Regular playback update callback - more generic name as requested
  private onPlaybackUpdate(): void {
    if (!this.isPlaying) {
      this.stopPlaybackUpdates();
      return;
    }

    // Calculate current playhead position based on elapsed time
    const elapsedMs = performance.now() - this.playbackStartTime;

    // Get playback delay from config
    const configManager = ConfigManager.instance();
    const playbackDelaySeconds = (configManager.get('audio.playback_delay') as number) ?? 0.2;
    const playbackDelayMs = playbackDelaySeconds * 1000;

    // Subtract the delay from elapsed time for visual sync
    // During the initial delay period, playhead stays at start position
    const adjustedElapsedMs = Math.max(0, elapsedMs - playbackDelayMs);

    const bpm = this.currentProject.getBpm();
    const beatsPerMs = bpm / (60 * 1000);
    let newPosition = this.playbackStartPosition + (adjustedElapsedMs * beatsPerMs);

    // Handle looping or end-of-project
    const beatsPerBar = this.currentProject.getTimeSignature().numerator;

    if (this.currentProject.getIsLooping()) {
      // Loop mode: wrap playhead when it reaches loop end
      const [startBar, endBarOriginal] = this.currentProject.getLoopingRange();
      const endBar = (startBar === 0 && endBarOriginal === 0) ? this.currentProject.getMaxBars() : endBarOriginal;

      const loopStartBeats = startBar * beatsPerBar;
      const loopEndBeats = (endBar + 1) * beatsPerBar; // +1 because endBar is inclusive
      const loopLengthBeats = loopEndBeats - loopStartBeats;

      // Wrap playhead position within loop range
      if (newPosition >= loopEndBeats) {
        if (this.loopBoundaryReachedCallback) {
          const callback = this.loopBoundaryReachedCallback;
          this.loopBoundaryReachedCallback = null;
          this.setPlayheadPosition(loopEndBeats);
          callback(loopEndBeats);
          return;
        }

        // Calculate how far we've overshot and wrap back
        const overshot = newPosition - loopEndBeats;
        newPosition = loopStartBeats + (overshot % loopLengthBeats);

        // Reset timing reference to prevent drift accumulation
        const newElapsedBeats = newPosition - loopStartBeats;
        this.playbackStartTime = performance.now() - (newElapsedBeats / beatsPerMs) - playbackDelayMs;
        this.playbackStartPosition = loopStartBeats;
      }
    } else {
      // Non-looping mode: stop at project end
      const maxBars = this.currentProject.getMaxBars();
      const maxBeats = maxBars * beatsPerBar;

      if (newPosition >= maxBeats) {
        // Clamp to max and stop
        this.setPlayheadPosition(maxBeats);
        // Stop playback (non-blocking)
        this.stopPlaying();
        return;
      }
    }

    // Update playhead position
    this.setPlayheadPosition(newPosition);

    // TODO: Future enhancements
    // - Sync with Tone.Transport position for more accurate timing
    // - Handle tempo changes mid-playback
    // - Account for latency compensation
  }

  // selected items
  public getSelectedItems(): Selectable[] {
    return this.selectedItems;
  }

  public addSelectedItem(item: Selectable): void {
    this.selectedItems = this.selectedItems.filter(i => i.getId() !== item.getId());
    this.selectedItems.push(item);
    this.notifySelectionChanged();
  }

  public addSelectedItems(items: Selectable[]): void {
    const seenIds = new Set(items.map(item => item.getId()));
    this.selectedItems = this.selectedItems.filter(item => !seenIds.has(item.getId()));
    this.selectedItems.push(...items);
    this.notifySelectionChanged();
  }

  public removeSelectedItem(item: Selectable): void {
    this.selectedItems = this.selectedItems.filter(i => i.getId() !== item.getId());
    this.notifySelectionChanged();
  }

  public removeSelectedItems(items: Selectable[]): void {
    this.selectedItems = this.selectedItems.filter(i => !items.includes(i));
    this.notifySelectionChanged();
  }

  public clearSelectedItems(): void {
    this.selectedItems = [];
    this.notifySelectionChanged();
  }

  // copied items
  public getCopiedItems(): Selectable[] {
    return this.copiedItems;
  }

  public addCopiedItem(item: Selectable): void {
    this.copiedItems.push(item);
  }

  public addCopiedItems(items: Selectable[]): void {
    this.copiedItems.push(...items);
  }

  public removeCopiedItem(item: Selectable): void {
    this.copiedItems = this.copiedItems.filter(i => i.getId() !== item.getId());
  }
  
  public clearCopiedItems(): void {
    this.copiedItems = [];
  }

  public copySelectedItems(): void {
    // Clear existing copied items
    this.clearCopiedItems();
    
    const selectedItems = this.getSelectedItems();
    
    if (selectedItems.length === 0) {
      return;
    }
    
    // Direct copy using getCurrentType() for type identification
    const clonedItems: Selectable[] = [];
    
    selectedItems.forEach(item => {
      const currentType = item.getCurrentType();
      
      switch (currentType) {
        case 'KGMidiNote': {
          const note = item as KGMidiNote;
          const clonedNote = new KGMidiNote(
            generateUniqueId('KGMidiNote'),
            note.getStartBeat(),
            note.getEndBeat(),
            note.getPitch(),
            note.getVelocity()
          );
          clonedItems.push(clonedNote);
          break;
        }
        
        case 'KGMidiRegion': {
          const region = item as KGMidiRegion;
          const clonedRegion = new KGMidiRegion(
            generateUniqueId('KGMidiRegion'),
            region.getTrackId(),
            region.getTrackIndex(),
            region.getName(),
            region.getStartFromBeat(),
            region.getLength()
          );
          
          // Copy all notes within the region
          const originalNotes = region.getNotes();
          originalNotes.forEach(note => {
            const clonedNote = new KGMidiNote(
              generateUniqueId('KGMidiNote'),
              note.getStartBeat(),
              note.getEndBeat(),
              note.getPitch(),
              note.getVelocity()
            );
            clonedRegion.addNote(clonedNote);
          });
          region.getPitchBends().forEach(pitchBend => {
            clonedRegion.addPitchBend(new KGMidiPitchBend(
              generateUniqueId('KGMidiPitchBend'),
              pitchBend.getBeat(),
              pitchBend.getValue()
            ));
          });
          region.getControllerEventsByType().forEach((events, controller) => {
            events.forEach(controllerEvent => {
              clonedRegion.addControllerEvent(controller, new KGMidiControllerEvent(
                generateUniqueId('KGMidiControllerEvent'),
                controllerEvent.getBeat(),
                controllerEvent.getValue()
              ));
            });
          });
          
          clonedItems.push(clonedRegion);
          break;
        }
        
        case 'KGRegion': {
          const region = item as KGRegion;
          const clonedRegion = new KGRegion(
            generateUniqueId('KGRegion'),
            region.getTrackId(),
            region.getTrackIndex(),
            region.getName(),
            region.getStartFromBeat(),
            region.getLength()
          );
          clonedItems.push(clonedRegion);
          break;
        }
        
        default:
          console.warn(`Unknown item type for copying: ${currentType}`);
          break;
      }
    });
    
    // Add the cloned items to the clipboard
    this.addCopiedItems(clonedItems);
    
    console.log(`Copied ${clonedItems.length} items to clipboard (${selectedItems.map(item => item.getCurrentType()).join(', ')})`);
  }

  // Command system methods for undo/redo functionality

  /**
   * Execute a command through the command history system
   * @param command The command to execute
   */
  public executeCommand(command: KGCommand, options?: { rethrow?: boolean }): void {
    this.commandHistory.executeCommand(command, options);
  }

  /**
   * Undo the last executed command
   * @returns true if undo was successful, false otherwise
   */
  public undo(): boolean {
    return this.commandHistory.undo();
  }

  /**
   * Redo the last undone command
   * @returns true if redo was successful, false otherwise
   */
  public redo(): boolean {
    return this.commandHistory.redo();
  }

  /**
   * Check if undo operation is available
   */
  public canUndo(): boolean {
    return this.commandHistory.canUndo();
  }

  /**
   * Check if redo operation is available
   */
  public canRedo(): boolean {
    return this.commandHistory.canRedo();
  }

  /**
   * Get description of the next command that would be undone
   */
  public getUndoDescription(): string | null {
    return this.commandHistory.getUndoDescription();
  }

  /**
   * Get description of the next command that would be redone
   */
  public getRedoDescription(): string | null {
    return this.commandHistory.getRedoDescription();
  }

  /**
   * Clear all command history
   */
  public clearCommandHistory(): void {
    this.commandHistory.clear();
  }

  /**
   * Set callback for when command history changes (for UI updates)
   */
  public setOnCommandHistoryChanged(callback: () => void): void {
    this.commandHistory.setOnHistoryChanged(callback);
  }

  /**
   * Get current command history statistics for debugging
   */
  public getCommandHistoryStats(): { undoCount: number; redoCount: number; maxSize: number } {
    return this.commandHistory.getHistoryStats();
  }
} 
