import { KGAudioInterface } from '../audio-interface/KGAudioInterface';
import { useProjectStore } from '../../stores/projectStore';
import { KGMidiTrack } from '../track/KGMidiTrack';

export interface LiveMidiNoteActivityEvent {
  pitch: number;
  isNoteOn: boolean;
}

type LiveNoteActivityListener = (...args: [LiveMidiNoteActivityEvent]) => void;

/**
 * KGMidiInput - MIDI input manager for the DAW
 * Implements the singleton pattern for global MIDI device management
 * Handles Web MIDI API integration for keyboard input
 */
export class KGMidiInput {
  private static readonly PITCH_BEND_CENTER = 8192;
  private static readonly PITCH_BEND_MAX_OFFSET = 8192;
  private static readonly CONTROL_CHANGE_MODULATION = 1;
  private static readonly CONTROL_CHANGE_BREATH = 2;
  private static readonly CONTROL_CHANGE_CHANNEL_VOLUME = 7;
  private static readonly CONTROL_CHANGE_EXPRESSION = 11;
  private static readonly CONTROL_CHANGE_SUSTAIN = 64;
  private static readonly SUSTAIN_ON_THRESHOLD = 64;

  // Private static instance for singleton pattern
  private static _instance: KGMidiInput | null = null;

  // MIDI state
  private midiAccess: MIDIAccess | null = null;
  private isInitialized: boolean = false;
  private connectedInputs: Map<string, MIDIInput> = new Map();

  // Recording callbacks
  private onRecordNoteOn: ((pitch: number, velocity: number) => void) | null = null;
  private onRecordNoteOff: ((pitch: number) => void) | null = null;
  private onRecordPitchBend: ((value: number) => void) | null = null;
  private onRecordControlChange: ((controller: number, value: number) => void) | null = null;
  private liveNoteTrackOwnership: Map<number, string[]> = new Map();
  private sustainPolarityInverted: boolean | null = null;
  private liveNoteActivityListeners: LiveNoteActivityListener[] = [];

  // Private constructor to prevent direct instantiation
  private constructor() {
    console.log("KGMidiInput initialized");
  }

  /**
   * Get the singleton instance of KGMidiInput
   * Creates the instance if it doesn't exist yet
   */
  public static instance(): KGMidiInput {
    if (!KGMidiInput._instance) {
      KGMidiInput._instance = new KGMidiInput();
    }
    return KGMidiInput._instance;
  }

  /**
   * Initialize the MIDI input manager
   */
  public async initialize(): Promise<void> {
    if (this.isInitialized) {
      return;
    }

    try {
      console.log("KGMidiInput ready for MIDI access request");
      this.isInitialized = true;
    } catch (error) {
      console.error("Failed to initialize MIDI input manager:", error);
      throw error;
    }
  }

  /**
   * Request MIDI access from browser
   * This must be called after a user gesture (click, keydown, etc.)
   */
  public async requestMIDIAccess(): Promise<void> {
    if (this.midiAccess) {
      console.log("MIDI access already granted");
      return;
    }

    try {
      // Check if Web MIDI API is available
      if (!navigator.requestMIDIAccess) {
        throw new Error("Web MIDI API is not supported in this browser");
      }

      // Request MIDI access
      this.midiAccess = await navigator.requestMIDIAccess();
      console.log("MIDI access granted");

      // Set up device listeners
      this.setupDeviceListeners();

      // Connect to all existing inputs
      this.connectToAllInputs();
    } catch (error) {
      console.error("Failed to request MIDI access:", error);
      throw error;
    }
  }

  /**
   * Set up listeners for MIDI device connection/disconnection
   */
  private setupDeviceListeners(): void {
    if (!this.midiAccess) {
      return;
    }

    this.midiAccess.onstatechange = (event: MIDIConnectionEvent) => {
      const port = event.port;

      if (port && port.type === "input") {
        if (port.state === "connected") {
          console.log(`MIDI device connected: ${port.name}`);
          this.connectToInput(port as MIDIInput);
        } else if (port.state === "disconnected") {
          console.log(`MIDI device disconnected: ${port.name}`);
          this.disconnectFromInput(port.id);
        }
      }
    };
  }

  /**
   * Connect to all available MIDI inputs
   */
  private connectToAllInputs(): void {
    if (!this.midiAccess) {
      return;
    }

    this.midiAccess.inputs.forEach((input) => {
      this.connectToInput(input);
    });

    console.log(`Connected to ${this.connectedInputs.size} MIDI input device(s)`);
  }

  /**
   * Connect to a specific MIDI input
   */
  private connectToInput(input: MIDIInput): void {
    // Set up message handler
    input.onmidimessage = (event: MIDIMessageEvent) => {
      this.handleMIDIMessage(event);
    };

    // Store the input
    this.connectedInputs.set(input.id, input);

    console.log(`Listening to MIDI input: ${input.name} (${input.id})`);
  }

  /**
   * Disconnect from a specific MIDI input
   */
  private disconnectFromInput(inputId: string): void {
    const input = this.connectedInputs.get(inputId);
    if (input) {
      input.onmidimessage = null;
    }
    this.connectedInputs.delete(inputId);
  }

  /**
   * Handle incoming MIDI messages
   */
  private handleMIDIMessage(event: MIDIMessageEvent): void {
    if (!event.data) {
      return;
    }

    const [status, pitch, velocity] = event.data;

    // Extract command (high nibble) and channel (low nibble)
    const command = status & 0xf0;
    const channel = status & 0x0f;

    // Note On: command = 0x90 (144)
    if (command === 0x90 && velocity > 0) {
      console.log(`MIDI Note On: pitch=${pitch}, velocity=${velocity}, channel=${channel}`);
      this.emitLiveNoteActivity({ pitch, isNoteOn: true });
      this.triggerNoteOn(pitch, velocity);
      this.onRecordNoteOn?.(pitch, velocity);
    }
    // Note Off: command = 0x80 (128) or Note On with velocity 0
    else if (command === 0x80 || (command === 0x90 && velocity === 0)) {
      console.log(`MIDI Note Off: pitch=${pitch}, channel=${channel}`);
      this.emitLiveNoteActivity({ pitch, isNoteOn: false });
      this.triggerNoteOff(pitch);
      this.onRecordNoteOff?.(pitch);
    }
    // Control Change: command = 0xB0 (176)
    else if (command === 0xb0) {
      console.log(`MIDI Control Change: controller=${pitch}, value=${velocity}, channel=${channel}`);
      this.handleControlChange(pitch, velocity);
    }
    // Pitch Bend: command = 0xE0 (224)
    else if (command === 0xe0) {
      const pitchBendValue = (velocity << 7) | pitch;
      console.log(`MIDI Pitch Bend: value=${pitchBendValue}, channel=${channel}`);
      this.triggerPitchBend(this.normalizePitchBend(pitchBendValue));
      this.onRecordPitchBend?.(pitchBendValue);
    }
  }

  /**
   * Trigger note on - play sound for MIDI note
   */
  private triggerNoteOn(pitch: number, velocity: number): void {
    try {
      const selectedTrackId = this.getSelectedMidiTrackId();
      if (!selectedTrackId) {
        return;
      }

      // Get audio interface and start playing the note
      const audioInterface = KGAudioInterface.instance();
      if (audioInterface.getIsInitialized()) {
        // Try to start audio context if not started yet
        if (!audioInterface.getIsAudioContextStarted()) {
          audioInterface.startAudioContext().catch(() => {
            // Silently fail if still not allowed - browser policy
          });
        }

        // Trigger note attack if audio context is ready
        if (audioInterface.getIsAudioContextStarted()) {
          const latchedTracks = this.liveNoteTrackOwnership.get(pitch) ?? [];
          latchedTracks.push(selectedTrackId);
          this.liveNoteTrackOwnership.set(pitch, latchedTracks);
          audioInterface.triggerLiveMidiNoteAttack(selectedTrackId, pitch, velocity);
          console.log(`MIDI triggered note attack: pitch=${pitch}, velocity=${velocity}, track=${selectedTrackId}`);
        }
      }
    } catch (error) {
      console.error(`Error triggering MIDI note on (pitch ${pitch}):`, error);
    }
  }

  /**
   * Trigger note off - stop sound for MIDI note
   */
  private triggerNoteOff(pitch: number): void {
    try {
      const selectedTrackId = this.consumeLatchedTrackIdForPitch(pitch);
      if (!selectedTrackId) {
        return;
      }

      // Get audio interface and stop playing the note
      const audioInterface = KGAudioInterface.instance();
      if (audioInterface.getIsInitialized() && audioInterface.getIsAudioContextStarted()) {
        audioInterface.releaseLiveMidiNote(selectedTrackId, pitch);
        console.log(`MIDI released note: pitch=${pitch}, track=${selectedTrackId}`);
      }
    } catch (error) {
      console.error(`Error triggering MIDI note off (pitch ${pitch}):`, error);
    }
  }

  private triggerPitchBend(normalizedBend: number): void {
    try {
      const selectedTrackId = this.getSelectedMidiTrackId();
      if (!selectedTrackId) {
        return;
      }

      const audioInterface = KGAudioInterface.instance();
      if (audioInterface.getIsInitialized() && audioInterface.getIsAudioContextStarted()) {
        audioInterface.setLiveMidiPitchBend(selectedTrackId, normalizedBend);
      }
    } catch (error) {
      console.error(`Error applying MIDI pitch bend (${normalizedBend}):`, error);
    }
  }

  private normalizePitchBend(pitchBendValue: number): number {
    const normalizedBend = (pitchBendValue - KGMidiInput.PITCH_BEND_CENTER) / KGMidiInput.PITCH_BEND_MAX_OFFSET;
    return Math.max(-1, Math.min(1, normalizedBend));
  }

  private handleControlChange(controller: number, value: number): void {
    const selectedTrackId = this.getSelectedMidiTrackId();
    if (!selectedTrackId) {
      return;
    }

    const audioInterface = KGAudioInterface.instance();
    if (!audioInterface.getIsInitialized() || !audioInterface.getIsAudioContextStarted()) {
      return;
    }

    if (controller === KGMidiInput.CONTROL_CHANGE_SUSTAIN) {
      const isPressed = this.normalizeSustainPedalValue(value);
      this.onRecordControlChange?.(controller, isPressed ? 127 : 0);
      audioInterface.setLiveMidiSustain(
        selectedTrackId,
        isPressed
      );
      return;
    }

    this.onRecordControlChange?.(controller, value);

    if (
      controller === KGMidiInput.CONTROL_CHANGE_MODULATION ||
      controller === KGMidiInput.CONTROL_CHANGE_BREATH ||
      controller === KGMidiInput.CONTROL_CHANGE_CHANNEL_VOLUME ||
      controller === KGMidiInput.CONTROL_CHANGE_EXPRESSION
    ) {
      audioInterface.setLiveMidiExpression(selectedTrackId, value / 127);
    }
  }

  private getSelectedMidiTrackId(): string | null {
    const { selectedTrackId, tracks } = useProjectStore.getState();
    if (!selectedTrackId) {
      console.log('No track selected - MIDI input ignored');
      return null;
    }

    const selectedTrack = tracks.find((track) => track.getId().toString() === selectedTrackId);
    if (!(selectedTrack instanceof KGMidiTrack)) {
      console.log(`Selected track ${selectedTrackId} is not a MIDI track - MIDI input ignored`);
      return null;
    }

    return selectedTrackId;
  }

  private consumeLatchedTrackIdForPitch(pitch: number): string | null {
    const latchedTracks = this.liveNoteTrackOwnership.get(pitch);
    if (!latchedTracks || latchedTracks.length === 0) {
      return null;
    }

    const trackId = latchedTracks.pop() ?? null;
    if (latchedTracks.length === 0) {
      this.liveNoteTrackOwnership.delete(pitch);
    } else {
      this.liveNoteTrackOwnership.set(pitch, latchedTracks);
    }

    return trackId;
  }

  private normalizeSustainPedalValue(value: number): boolean {
    const rawPressed = value >= KGMidiInput.SUSTAIN_ON_THRESHOLD;

    if (this.sustainPolarityInverted === null) {
      // Assume the pedal starts released. The first observed sustain CC therefore
      // represents a press-down gesture and reveals whether the device is inverted.
      this.sustainPolarityInverted = !rawPressed;
    }

    return this.sustainPolarityInverted ? !rawPressed : rawPressed;
  }

  private emitLiveNoteActivity(event: LiveMidiNoteActivityEvent): void {
    for (const listener of this.liveNoteActivityListeners) {
      try {
        listener(event);
      } catch {
        // Swallow listener errors to avoid disrupting MIDI handling.
      }
    }
  }

  /**
   * Clean up MIDI resources
   */
  public async dispose(): Promise<void> {
    try {
      // Disconnect from all inputs
      this.connectedInputs.forEach((input, inputId) => {
        this.disconnectFromInput(inputId);
      });
      this.connectedInputs.clear();

      // Clear MIDI access
      if (this.midiAccess) {
        this.midiAccess.onstatechange = null;
        this.midiAccess = null;
      }

      this.isInitialized = false;
      this.liveNoteTrackOwnership.clear();
      this.sustainPolarityInverted = null;
      this.liveNoteActivityListeners = [];

      console.log("MIDI resources disposed successfully");
    } catch (error) {
      console.error("Error disposing MIDI resources:", error);
    }
  }

  // ===== RECORDING =====

  public setRecordingCallbacks(
    onNoteOn: ((pitch: number, velocity: number) => void) | null,
    onNoteOff: ((pitch: number) => void) | null,
    onPitchBend: ((value: number) => void) | null = null,
    onControlChange: ((controller: number, value: number) => void) | null = null
  ): void {
    this.onRecordNoteOn = onNoteOn;
    this.onRecordNoteOff = onNoteOff;
    this.onRecordPitchBend = onPitchBend;
    this.onRecordControlChange = onControlChange;
  }

  public addLiveNoteActivityListener(listener: LiveNoteActivityListener): void {
    this.liveNoteActivityListeners.push(listener);
  }

  public removeLiveNoteActivityListener(listener: LiveNoteActivityListener): void {
    this.liveNoteActivityListeners = this.liveNoteActivityListeners.filter(current => current !== listener);
  }

  // ===== GETTERS =====

  public getIsInitialized(): boolean {
    return this.isInitialized;
  }

  public getMIDIAccess(): MIDIAccess | null {
    return this.midiAccess;
  }

  public getConnectedInputs(): MIDIInput[] {
    return Array.from(this.connectedInputs.values());
  }

  public getConnectedInputCount(): number {
    return this.connectedInputs.size;
  }
}
