type AudioTrackMap = Record<string, string>;

type PlayEffectOptions = {
  volume?: number;
  rate?: number;
  stopAfterMs?: number;
};

class SoundDirector {
  private context?: AudioContext;
  private master?: GainNode;
  private started = false;
  private muted = false;
  private musicTracks: AudioTrackMap = {};
  private effectTracks: AudioTrackMap = {};
  private music?: HTMLAudioElement;
  private currentMusicKey?: string;
  private pendingMusicKey?: string;
  private musicFadeTimer?: number;
  private readonly musicVolume = 0.22;
  private readonly effectVolume = 0.42;

  registerMusicTracks(tracks: AudioTrackMap) {
    this.musicTracks = tracks;
  }

  registerEffectTracks(tracks: AudioTrackMap) {
    this.effectTracks = tracks;
  }

  start() {
    if (this.started) {
      this.resume();
      if (this.pendingMusicKey) {
        this.playMusic(this.pendingMusicKey);
      }
      return;
    }

    const AudioContextCtor = window.AudioContext ?? window.webkitAudioContext;
    if (AudioContextCtor) {
      this.context = new AudioContextCtor();
      this.master = this.context.createGain();
      this.master.gain.value = this.muted ? 0 : 0.3;
      this.master.connect(this.context.destination);
    }

    this.started = true;
    this.resume();
    if (this.pendingMusicKey) {
      this.playMusic(this.pendingMusicKey);
    }
  }

  resume() {
    void this.context?.resume();
    if (this.started && this.music) {
      void this.music.play().catch(() => undefined);
    }
  }

  setMuted(muted: boolean) {
    this.muted = muted;
    if (this.music) {
      this.music.muted = muted;
    }

    if (!this.context || !this.master) {
      return;
    }

    this.master.gain.cancelScheduledValues(this.context.currentTime);
    this.master.gain.linearRampToValueAtTime(muted ? 0 : 0.3, this.context.currentTime + 0.18);
  }

  isMuted() {
    return this.muted;
  }

  playHover() {
    this.playTone(540, 0.04, 0.018, 'sine');
  }

  playSelect() {
    if (this.playEffect('ui-select', { volume: 0.24, stopAfterMs: 420 })) {
      return;
    }

    this.playTone(300, 0.07, 0.04, 'sine');
    window.setTimeout(() => this.playTone(450, 0.08, 0.028, 'sine'), 70);
  }

  playGrowthTick() {
    if (this.playEffect('exp-gain', { volume: 0.18, rate: 1.12, stopAfterMs: 360 })) {
      return;
    }

    this.playTone(620, 0.05, 0.018, 'triangle');
  }

  playLevelUp() {
    this.playEffect('exp-gain', { volume: 0.42, rate: 1.08, stopAfterMs: 820 });
    this.playTone(520, 0.08, 0.035, 'triangle');
    window.setTimeout(() => this.playTone(720, 0.09, 0.04, 'triangle'), 82);
    window.setTimeout(() => this.playTone(980, 0.12, 0.034, 'sine'), 168);
  }

  playEffect(key: string, options: PlayEffectOptions = {}) {
    if (!this.started || this.muted) {
      return false;
    }

    const src = this.effectTracks[key];
    if (!src) {
      return false;
    }

    const effect = new Audio(src);
    effect.preload = 'auto';
    effect.volume = options.volume ?? this.effectVolume;
    effect.playbackRate = options.rate ?? 1;
    effect.muted = this.muted;
    effect.addEventListener(
      'ended',
      () => {
        effect.src = '';
      },
      { once: true }
    );

    if (options.stopAfterMs) {
      window.setTimeout(() => {
        effect.pause();
        effect.src = '';
      }, options.stopAfterMs);
    }

    void effect.play().catch(() => undefined);
    return true;
  }

  playMusic(key?: string) {
    if (!key) {
      return;
    }

    this.pendingMusicKey = key;
    if (!this.started) {
      return;
    }

    const src = this.musicTracks[key];
    if (!src) {
      return;
    }

    if (this.currentMusicKey === key && this.music) {
      this.music.muted = this.muted;
      void this.music.play().catch(() => undefined);
      return;
    }

    const previous = this.music;
    const next = new Audio(src);
    next.loop = true;
    next.preload = 'auto';
    next.muted = this.muted;
    next.volume = 0;

    this.music = next;
    this.currentMusicKey = key;
    void next.play().catch(() => undefined);
    this.fadeMusic(previous, next, this.musicVolume);
  }

  private playTone(
    frequency: number,
    duration: number,
    gainValue: number,
    type: OscillatorType
  ) {
    if (!this.context || this.muted) {
      return;
    }

    const oscillator = this.context.createOscillator();
    const gain = this.context.createGain();
    const now = this.context.currentTime;

    oscillator.type = type;
    oscillator.frequency.value = frequency;
    gain.gain.setValueAtTime(0, now);
    gain.gain.linearRampToValueAtTime(gainValue, now + 0.012);
    gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);

    oscillator.connect(gain);
    gain.connect(this.master ?? this.context.destination);
    oscillator.start(now);
    oscillator.stop(now + duration + 0.04);
  }

  private fadeMusic(previous: HTMLAudioElement | undefined, next: HTMLAudioElement, targetVolume: number) {
    if (this.musicFadeTimer) {
      window.clearInterval(this.musicFadeTimer);
    }

    const duration = 900;
    const startedAt = performance.now();
    const previousStartVolume = previous?.volume ?? 0;

    this.musicFadeTimer = window.setInterval(() => {
      const progress = Math.min(1, (performance.now() - startedAt) / duration);
      next.volume = targetVolume * progress;

      if (previous) {
        previous.volume = previousStartVolume * (1 - progress);
      }

      if (progress >= 1) {
        if (this.musicFadeTimer) {
          window.clearInterval(this.musicFadeTimer);
          this.musicFadeTimer = undefined;
        }
        if (previous) {
          previous.pause();
          previous.src = '';
        }
      }
    }, 32);
  }

}

declare global {
  interface Window {
    webkitAudioContext?: typeof AudioContext;
  }
}

export const soundDirector = new SoundDirector();
