/*
  Partial IAudioPlayer implementation using the high level "soundfont-player" library.
 */

import { MidiInstrument } from "..";
import { IAudioPlayer } from "../Common/Interfaces/IAudioPlayer";
import { AudioContext as SAudioContext } from "standardized-audio-context";
import * as SoundfontPlayer from "soundfont-player";
import midiNames from "./midiNames";
import noteName from "./noteName";

function nameToUrl (name: string, sf: string, format: string): string {
  format = format === "ogg" ? format : "mp3";
  // sf = sf === 'FluidR3_GM' ? sf : 'MusyngKite'
  // sound_source_1   sound_source_2
  return 'https://ks3-cn-beijing.ksyuncs.com/daya/sound_source_1/' + name + '-' + format + '.js'
  // return "/music/" + name + "-" + format + ".js";
}
export class BasicAudioPlayer implements IAudioPlayer<SoundfontPlayer.Player> {

  constructor(osmd?: any) {
    this.instruments = osmd.sheet.instruments;
  }

  private instruments: any[];
  private ac: SAudioContext = new SAudioContext();
  // private mainTuningRatio: number = 1.0;
  private channelVolumes: number[] = [];
  // private activeSamples: Map<number, any> = new Map();
  private piano: SoundfontPlayer.Player;

  protected memoryLoadedSoundFonts: Map<MidiInstrument, SoundfontPlayer.Player> = new Map();
  protected channelToSoundFont: Map<number, number> = new Map();

  public SoundfontInstrumentOptions = {
    only: [],
    // from: 'https://ks3-cn-beijing.ksyuncs.com/daya/audio/',
    nameToUrl,
  }; // e.g. set { from: 'server.com/soundfonts/' } for soundfont fetching url

  public async open(uniqueInstruments: number[], numberOfinstruments: number = 16): Promise<void> {
    if (this.piano === undefined) {
      this.piano = await SoundfontPlayer.instrument(
        this.ac as unknown as AudioContext,
        midiNames[MidiInstrument.Acoustic_Grand_Piano].toLowerCase() as any,
        {
          ...this.SoundfontInstrumentOptions,
          only: undefined,
        }
      );
    }
    for (let i: number = 0; i < numberOfinstruments; i++) {
      this.channelVolumes[i] = 0.8;
    }
  }

  public close(): void {
    // _activeSamples.Clear();
  }

  public tuningChanged(tuningInHz: number): void {
    console.warn("BasicAudioPlayer tuningChanged not implemented");
    //this.mainTuningRatio = tuningInHz / 440;
  }

  public playSound(
    instrumentChannel: number,
    key: number,
    volume: number,
    lengthInMs: number
  ): void {
    if (key >= 128) { return; }

    const sampleVolume: number = Math.min(
      1,
      this.channelVolumes[instrumentChannel] * volume
    );

    const soundFont: SoundfontPlayer.Player = this.memoryLoadedSoundFonts.get(
      this.channelToSoundFont.get(instrumentChannel)
    );
    // console.log(soundFont, key, sampleVolume, lengthInMs);
    soundFont.schedule(0, [
      { note: key, duration: lengthInMs / 1000, gain: sampleVolume },
    ]);
  }

  public stopSound(instrumentChannel: number, volume: number): void {
    // 此处是官方注释，实现立即停止播放剩余的声音
    // this.memoryLoadedSoundFonts.get(this.channelToSoundFont.get(instrumentChannel))?.stop(); // abrupt
    //console.warn("BasicAudioPlayer stopSound not implemented");
  }

  public getUsedSoundFonts(instrument: any, instrumentChannel: number): string[] {
    // eslint-disable-next-line @typescript-eslint/typedef
    const noteKeys = new Set<string>();
    for (const voice of instrument.voices) {
      for (const entry of voice.voiceEntries) {
        for (const note of entry.notes) {
          if (note.halfTone > 0) {
            noteKeys.add(noteName[note.halfTone + 12]);
          }
        }
      }
    }
    return Array.from(noteKeys);
  }

  public async setSound(
    instrumentChannel: number,
    soundId: MidiInstrument
  ): Promise<void> {
    // console.log(instrumentChannel, soundId, this.instruments[instrumentChannel]);
    console.log(this.getUsedSoundFonts(this.instruments[instrumentChannel], instrumentChannel));
    this.SoundfontInstrumentOptions.only = this.getUsedSoundFonts(this.instruments[instrumentChannel], instrumentChannel);
    if (this.memoryLoadedSoundFonts.get(soundId) === undefined) {
      await this.loadSoundFont(soundId);
    }
    this.channelToSoundFont.set(instrumentChannel, soundId);
    return Promise.resolve();
  }

  public async loadSoundFont(soundId: MidiInstrument): Promise<SoundfontPlayer.Player> {
    if (this.memoryLoadedSoundFonts.get(soundId) !== undefined) {
      return this.memoryLoadedSoundFonts.get(soundId);
    }
    // console.log(soundId, midiNames[soundId].toLowerCase())
    const player: SoundfontPlayer.Player = await SoundfontPlayer.instrument(
      this.ac as unknown as AudioContext,
      midiNames[soundId].toLowerCase() as any,
      this.SoundfontInstrumentOptions
    );
    // console.log(this.memoryLoadedSoundFonts, player)
    this.memoryLoadedSoundFonts.set(soundId, {
      ...player,
    });
    return player;
  }

  public setVolume(instrumentChannel: number, volume: number): void {
    this.channelVolumes[instrumentChannel] = volume;
  }

  public setSoundFontFilePath(soundId: MidiInstrument, path: string): void {
    // TODO: Remove function, not needed for web. If not used to load different soundfonts from URLs?
  }

  public playbackHasStopped(): void {
    //console.warn("BasicAudioPlayer playbackHasStopped not implemented");
  }

  public getMemoryLoadedSoundFonts(): SoundfontPlayer.Player[] {
    return [...this.memoryLoadedSoundFonts.values()];
  }
}
