import { ITimingSource } from "../Common/Interfaces/ITimingSource";
import { IMessageViewer } from "../Common/Interfaces/IMessageViewer";
import { IAudioPlayer } from "../Common/Interfaces/IAudioPlayer";
import { MusicPartManager, MusicPartManagerIterator } from "../MusicalScore/MusicParts";
import { PlaybackIterator } from "../MusicalScore/Playback/PlaybackIterator";
import { Dictionary } from "typescript-collections";
import { Staff, SourceMeasure, VoiceEntry, Note, MidiInstrument } from "../MusicalScore/VoiceData";
import { Fraction } from "../Common/DataObjects";
import { MetronomeInstrument } from "./MetronomeInstrument";
import { CursorPosChangedData } from "../Common/DataObjects/CursorPosChangedData";
import { Repetition } from "../MusicalScore/MusicSource";
import { TextTranslation } from "../Common/Strings/TextTranslation";
import { NoteState, Instrument, SubInstrument, MusicSheet } from "../MusicalScore";
import { DynamicsContainer } from "../MusicalScore/VoiceData/HelperObjects";
import { PlaybackEntry } from "../MusicalScore/Playback/PlaybackEntry";
import { ContinuousDynamicExpression } from "../MusicalScore/VoiceData/Expressions/ContinuousExpressions";
import { PlaybackNote } from "../MusicalScore/Playback/PlaybackNote";
import log from "loglevel";
import { IAudioMetronomePlayer } from "../Common/Interfaces/IAudioMetronomePlayer";
import { ISettableInstrument } from "../Common/Interfaces/ISettableInstrument";
import { PlaybackState, MessageBoxType } from "../Common/Enums/PsEnums";
import { IPlaybackListener } from "../Common/Interfaces/IPlaybackListener";
import { IPlaybackParametersListener } from "../Common/Interfaces/IPlaybackParametersListener";
import { AbstractExpression } from "../MusicalScore/VoiceData/Expressions";

export class ChannelNote {
    public note: PlaybackNote;
    public key: number;
    public channel: number;

    constructor(k: number, c: number, n: PlaybackNote = undefined) {
        this.note = n;
        this.key = k;
        this.channel = c;
    }
}

export class PlaybackManager implements IPlaybackParametersListener {
    protected timingSource: ITimingSource;
    protected resetRequested: boolean;
    protected loopTriggeredReset: boolean;
    protected tempoUserFactor: number;
    protected currentBPM: number;
    protected listeners: IPlaybackListener[] = [];
    public addListener(listener: IPlaybackListener): void {
        this.listeners.push(listener);
    }
    private readonly percussionChannel: number = 9; // this is a definition of the midi interface (cannot be changed)
    private messageViewer: IMessageViewer;
    private audioMetronomePlayer: IAudioMetronomePlayer;
    private audioPlayer: IAudioPlayer<any>;
    private musicPartManager: MusicPartManager;
    private cursorIterator: MusicPartManagerIterator;
    get CursorIterator(): MusicPartManagerIterator {
        return this.cursorIterator;
    }
    private playbackIterator: PlaybackIterator;
    //private Dictionary<int, MidiChannelInfo> instrumentsPerMidiSoundDict = new Dictionary<int, MidiChannelInfo>();
    //private Dictionary<int, int> midiSoundToChannelMappingDict = new Dictionary<int, int>();
    //private int[] midiChannelToSoundArray = new int[16];
    //Staff is not considered Unique for key purposes here. Had to use something unique - staff ID
    private instrumentToStaffToMidiChannelDict: Dictionary<Staff, number> = new Dictionary<Staff, number>();
    //store this data just in case
    private instrumentIdMapping: Dictionary<number, Instrument> = new Dictionary<number, Instrument>();
    public get InstrumentIdMapping(): Dictionary<number, Instrument> {
        return this.instrumentIdMapping;
    }
    //private List<int> staffIndexToMidiChannelMapping = new List<int>();
    private freeMidiChannels: number[] = [];
    private notesToStop: Dictionary<Fraction, ChannelNote[]> = new Dictionary<Fraction, ChannelNote[]>();

    private metronomeNote: ChannelNote = new ChannelNote(88, this.percussionChannel);
    private metronomeNoteFirstBeat: ChannelNote = new ChannelNote(64, this.percussionChannel);


    private currentMeasure: SourceMeasure = undefined;
    private currentTimestamp: Fraction = undefined;
    private closestNextTimestamp: Fraction = undefined;
    private currentMetronomeBaseTimestamp: Fraction = undefined;
    private currentBeatDuration: Fraction = undefined;
    private currentIteratorSourceTimeStamp: Fraction = undefined;
    private beatCounter: number = 0;

    protected runningState: PlaybackState = PlaybackState.Stopped;
    private isRunning: boolean = false;
    private isInitialized: boolean = false;     // make sure midi device gets opened only once
    private nextIteratorTimestamp: Fraction;
    private playNextMetronomeAt: Fraction;
    // private masterTranspose: number = 0;

    private isPlaying: boolean = false;
    private metronome: MetronomeInstrument;
    private metronomeSoundPlayed: boolean = false;


    private tempoImpactFactor: number = 1.0;
    private sheetStartBPM: number;
    private currentReferenceBPM: number;
    private readonly defaultVolume: number = 0.8;
    private currentVolume: number = this.defaultVolume;
    private dynamicImpactFactor: number = 1.0;
    private scorePositionChangedData: CursorPosChangedData = new CursorPosChangedData();
    private tooManyInstruments: boolean = false;

    private currentRepetition: Repetition;
    private currentMeasureIndex: number;
    private metronomeOnlyBPM: number = 100;
    // private playbackThreadSyncObject = new object(); // TODO MB: Handle this.
    private readonly highlightPlayedNotes: boolean = false;

    private startRhythmBeats: number;
    private startRhythmDenominator: number;
    private isPreCounting: boolean;
    private fermataActive: boolean;
    private doPreCount: boolean = true;

    constructor (timingSource: ITimingSource, audioMetronomePlayer: IAudioMetronomePlayer, audioPlayer: IAudioPlayer<any>, messageViewer: IMessageViewer) {
        const metronomeLabel: string = TextTranslation.translateText("Playback/LabelMetronome", "Metronome");
        this.metronome = new MetronomeInstrument(-1, metronomeLabel, false, true, 0.0, MidiInstrument.Percussion);
        this.timingSource = timingSource;
        this.audioMetronomePlayer = audioMetronomePlayer;
        this.audioPlayer = audioPlayer;
        this.messageViewer = messageViewer;
    }

    public get RunningState(): PlaybackState {
        return this.runningState;
    }
    public set RunningState(value: PlaybackState) {
        this.runningState = value;
    }

    public DoPlayback: boolean;

    /** Do the initial pre-count */
    public get DoPreCount(): boolean {
        return this.doPreCount;
    }
    public set DoPreCount(value: boolean) {
        if (this.doPreCount !== value) {
            this.doPreCount = value;
        }
    }

    public PreCountBeats: number;

    public get Metronome(): ISettableInstrument {
        return this.metronome;
    }

    public get MetronomeOnlyBPM(): number {
        return this.metronomeOnlyBPM;
    }
    public set MetronomeOnlyBPM(value: number) {
        this.metronomeOnlyBPM = value;
    }

    // public get Transpose(): number {
    //     return this.masterTranspose;
    // }
    // public set Transpose(value: number) {
    //     this.masterTranspose = value;
    // }

    public get OriginalBpm(): number {
        return this.currentReferenceBPM;
    }

    /** will be activated when any solo flag of an Instrument, Voice or Staff is set to true. */
    public SoloActive: boolean;
    public SoloAttenuationValue: number = 0;

    // Only used for debug and scheduling precision measurements
    //private wantedNextIteratorTimestampMs: number = 0;

    public playVoiceEntry(voiceEntry: VoiceEntry): void {
        const ve: VoiceEntry = voiceEntry;
        if (ve !== undefined) {
            // lock(this.playbackThreadSyncObject) {
                this.stopAllCurrentlyPlayingNotes();

                if (this.highlightPlayedNotes) {
                    const notes: Note[] = [];
                    for (const note of ve.Notes) {
                        note.state = NoteState.Selected;
                        notes.push(note);
                    }
                    //this.NotesPlaybackEventOccurred(notes);
                }

                //int staffIndex = this.musicPartManager.MusicSheet.getIndexFromStaff(ve.Notes[0].ParentStaff);
                const channel: number = this.instrumentToStaffToMidiChannelDict.getValue(ve.Notes[0].ParentStaff);
                const instrument: Instrument = ve.ParentVoice.Parent;
                const isPercussion: boolean = instrument.MidiInstrumentId === MidiInstrument.Percussion;
                const volume: number = 0.8;
                const notesToPlay: ChannelNote[] = [];

                const transpose: number = this.musicPartManager.MusicSheet.Transpose;
                const instrumentPlaybackTranspose: number = ve.ParentVoice.Parent.PlaybackTranspose;

                for (const note of ve.MainPlaybackEntry.Notes.filter(n => n.MidiKey !== 0)) {
                    // play the note
                    let key: number = note.MidiKey;
                    if (!isPercussion) {
                        key += instrumentPlaybackTranspose + transpose;
                    }
                    if (note.ParentNote.PlaybackInstrumentId !== undefined) {
                        const notePlaybackInstrument: SubInstrument =
                            instrument.getSubInstrument(note.ParentNote.PlaybackInstrumentId);
                        if (notePlaybackInstrument !== undefined) {
                            if (notePlaybackInstrument.fixedKey >= 0) {
                                key = notePlaybackInstrument.fixedKey;
                            }
                        }
                    }

                    // calculate stop time and remember it
                    // const stopAt: Fraction = Fraction.plus(this.cursorIterator.CurrentEnrolledTimestamp, note.Length);
                    try {
                        if (this.audioPlayer !== undefined) {
                            //const noteLengthFraction: Fraction = Fraction.createFromFraction(note.Length);
                            this.audioPlayer.playSound(channel, key, volume, 500);
                        }
                    } catch (ex) {
                        log.info("PlaybackManager.playVoiceEntry: ", ex);
                    }

                    notesToPlay.push(new ChannelNote(key, channel, note));
                }

                // TODO MB: Handle this
                // Task stopper = new Task(() => {
                //     EventWaitHandle waiter = new EventWaitHandle(false, EventResetMode.AutoReset);
                //     waiter.WaitOne(200);
                //     lock(this.playbackThreadSyncObject) {
                //         if (this.audioPlayer !== undefined) {
                //             foreach(var n in notesToPlay) {
                //                 this.audioPlayer.stopSound(n.channel, n.key);
                //             }
                //     }
                //     }

                //     // redraw to color notes normal if highlighted in playback
                //     //this.phonicScoreInterface.RedrawGraphicalMusicSheet();
                // });
                // stopper.Start();
            // }
        }
    }


    public initialize(musicPartMng: MusicPartManager): void {
        // lock(this.playbackThreadSyncObject) {
        if (this.isInitialized) {
            this.stopAllCurrentlyPlayingNotes();
            if (this.audioPlayer !== undefined) {
                this.audioPlayer.close();
            }

            this.cursorIterator = undefined;
            this.playbackIterator = undefined;
        }

        this.isInitialized = false;

        this.musicPartManager = musicPartMng;
        if (this.musicPartManager !== undefined) {
            const musicSheet: MusicSheet = this.musicPartManager.MusicSheet;
            // TODO MB: Converted musicSheetParameterChanged to setBpm in this file. Handle following line.
            //musicSheet.MusicSheetParameterChanged += this.musicSheetParameterChanged;

            this.cursorIterator = this.musicPartManager.getIterator();
            this.playbackIterator = new PlaybackIterator(musicSheet);

            if (this.audioPlayer !== undefined) {
                // TODO MB: I rewrote following line in line below. Does it do what it's supposed to do? Array.from() not supported in IE
                // List < MidiInstrument > uniqueMidiInstruments = musicSheet.Instruments.Select(item => item.MidiInstrumentId).Distinct().ToList();
                const uniqueMidiInstruments: MidiInstrument[] = Array.from(new Set(musicSheet.Instruments.map(item => item.MidiInstrumentId)));

                this.audioPlayer.open(uniqueMidiInstruments, 16);
                // set drums:
                this.audioPlayer.setSound(this.percussionChannel, 115);
            }

            this.currentReferenceBPM = this.sheetStartBPM = musicSheet.getExpressionsStartTempoInBPM();
            this.tempoUserFactor = musicSheet.userStartTempoInBPM / this.sheetStartBPM;
            let instrumentId: number = 0;
            this.tooManyInstruments = false;

            // reset the dicts and channel mappings
            //this.staffIndexToMidiChannelMapping.Clear();
            this.instrumentToStaffToMidiChannelDict.clear();
            this.instrumentIdMapping.clear();
            for (let i: number = 0; i < this.percussionChannel; i++) {
                this.freeMidiChannels.push(i);
            }

            for (let i: number = this.percussionChannel + 1; i < 16; i++) {
                this.freeMidiChannels.push(i);
            }

            for (const instrument of musicSheet.Instruments) {
                this.instrumentIdMapping.setValue(instrumentId, instrument);

                for (const staff of instrument.Staves) {
                    // just add a list element - calcMidiChannel() will provide the right value.
                    //this.staffIndexToMidiChannelMapping.Add(-1);
                    this.instrumentToStaffToMidiChannelDict.setValue(staff, -1);
                }
                this.setSound(instrumentId, instrument.MidiInstrumentId);

                instrumentId++;
            }

            if (this.audioPlayer !== undefined && this.tooManyInstruments) {
                const errorMsg: string = TextTranslation.translateText(
                    "MidiNumberError",
                    "This music sheet has more parts than are supported for midi playback. " +
                    "Some parts will not be played with the desired instrument sounds."
                    );
                if (this.messageViewer !== undefined && this.messageViewer.MessageOccurred !== undefined) {
                    this.messageViewer.MessageOccurred(MessageBoxType.Warning, errorMsg);
                }
            }

            this.checkForSoloDeactivated();
        }

        this.isInitialized = true;
        // }

        this.reset();
    }

    public async play(): Promise<void> {
        if (this.cursorIterator !== undefined && this.cursorIterator.EndReached) {
            console.log("End reached, resetting");
            this.reset();
        }

        this.isPlaying = true;
        this.RunningState = PlaybackState.Running;
        await this.timingSource.start();
        this.loop();
    }

    public async pause(): Promise<void> {
        // lock(this.playbackThreadSyncObject) {
            this.isPlaying = false;
            // stop all active midi notes:
            this.stopAllCurrentlyPlayingNotes();

            // inform sample player to e.g. dispose used samples:
            if (this.audioPlayer !== undefined) {
                this.audioPlayer.playbackHasStopped();
            }

            // notify delegates (coreContainer) that the playing has finished:
            this.RunningState = PlaybackState.Stopped;
            await this.timingSource.pause();
            try {
                //bool endReached = this.iterator !== undefined && this.iterator.EndReached;
                for (const listener of this.listeners) {
                    listener.pauseOccurred(undefined);
                }
            } catch (ex) {
                log.debug("PlaybackManager.pause: ", ex);
            }
        // }
    }

    public reset(): void {
        // lock(this.playbackThreadSyncObject) {
        //this.resetRequested = true;
        this.doReset(this.DoPreCount);

        if (this.musicPartManager === undefined) {
            return;
        }

        if (this.RunningState === PlaybackState.Stopped) {
            //this.isPlaying = true;
        }
        for (const listener of this.listeners) {
            listener.resetOccurred(undefined);
        }
        // }

    }

    public Dispose(): void {
        // lock(this.playbackThreadSyncObject) {
            this.listeners = [];
            this.isRunning = false;
            // stop all active midi notes:
            if (this.isInitialized) {
                this.stopAllCurrentlyPlayingNotes();
                if (this.audioPlayer !== undefined) {
                    this.audioPlayer.close();
                }
            }

    //         this.musicPartManager = undefined;
    //     // }
    }

    public setSound(instrumentId: number, newSoundId: MidiInstrument): boolean {
        if (newSoundId <= MidiInstrument.None || newSoundId > MidiInstrument.Percussion) {
            return false;
        }

        // lock(this.playbackThreadSyncObject) {
        try {
            const isPercussionNow: boolean = newSoundId === MidiInstrument.Percussion;

            if (instrumentId === -1) { // Metronome
                if (this.audioPlayer !== undefined && !isPercussionNow) {
                    this.audioPlayer.setSound(0, newSoundId);
                }
            } else {
                let neededLastChannel: boolean = false;
                const musicSheet: MusicSheet = this.musicPartManager.MusicSheet;
                let instrument: Instrument;

                if (instrumentId === -2) {
                    instrument = musicSheet.Instruments.find(x => x.Id === instrumentId);
                } else {
                    instrument = musicSheet.Instruments[instrumentId];
                }

                this.instrumentIdMapping.setValue(instrument.Id, instrument);

                for (const staff of instrument.Staves) {
                    //int staffIndex = musicSheet.getIndexFromStaff(staff);
                    //int channel = this.staffIndexToMidiChannelMapping[staffIndex];
                    let channel: number = this.instrumentToStaffToMidiChannelDict.getValue(staff);
                    const wasPercussion: boolean = channel === this.percussionChannel;

                    if (isPercussionNow) { // if is now a percussion
                        const oldChannel: number = channel;
                        channel = this.percussionChannel;
                        // check if this instrument has been initialized and was no percussion instrument:
                        if (oldChannel > 0 && !wasPercussion) {
                            this.freeMidiChannels.push(oldChannel);
                            this.freeMidiChannels.sort((a, b) => a - b); //TODO MB: Does this .sort do the same thing as C# .Sort()?
                        }
                    } else {
                        if (channel < 0 || wasPercussion) { // if is not initialized or was a percussion:
                            if (this.freeMidiChannels.length > 0) { // if still a free channel exists
                                // get the channel and remove in from the free channels list
                                channel = this.freeMidiChannels[0];
                                this.freeMidiChannels.shift();
                            } else { // if no channel is free any more:
                                this.tooManyInstruments = true;
                                // use last channel
                                channel = 15;
                                this.instrumentToStaffToMidiChannelDict.setValue(staff, channel);

                                //// use piano sound
                                //newSoundId = 0;
                                neededLastChannel = true;
                            }
                        }
                    }
                    this.instrumentToStaffToMidiChannelDict.setValue(staff, channel);
                    if (this.audioPlayer !== undefined && !isPercussionNow) {
                        // TODO: Uncomment when panaroma is supported in audio player
                        // this.audioPlayer.setPanorama(channel, instrument.SubInstruments[0].pan);

                        // TODO: Commented because AvailableComponents not defined
                        // if (AvailableComponents.PLAYBACK_INSTRUMENTS_AVAILABLE) {
                        //     // only set instrument sounds in pro version:
                        this.audioPlayer.setSound(channel, newSoundId);
                        // } else {
                            // play all as piano in free version:
                        //    this.audioPlayer.setSound(channel, 0);
                        // }
                    }
                }
                if (neededLastChannel) {
                    return false;
                }
            }

            return true;
        } catch (ex) {
            log.info("PlaybackManager.setSound: ", ex);
            return false;
        }
        // }
    }

    // public mainParameterChanged(client: IPhonicScoreClient, settingType: ProgramParameters, currentValue, previousValue): void {
    //     switch (settingType) {
    //         case ProgramParameters.DynamicInstructionsImpact:
    //             this.dynamicImpactFactor = Convert.ToSingle(currentValue);
    //             break;
    //         case ProgramParameters.TempoInstructionsImpact: {
    //             this.tempoImpactFactor = Convert.ToSingle(currentValue);
    //             this.setTempo();
    //             break;
    //         }
    //     }
    // }

    // TODO MB: Check if function setBpm() is sufficient for doing what commented function below does.
    // protected musicSheetParameterChanged(client: IPhonicScoreClient, parameter: MusicSheetParameters, currentValue, previousValue): void {
    //     switch (parameter) {
    //         case MusicSheetParameters.StartTempoInBPM: {
    //             this.tempoUserFactor = Convert.ToSingle(currentValue) / this.sheetStartBPM;
    //             this.setTempo();
    //             break;
    //         }
    //     }
    // }
    protected setBpm(bpm: number): void {
        this.tempoUserFactor = bpm / this.sheetStartBPM;
        this.setTempo();
    }

    public handlePlaybackEvent(): void {
        // lock(this.playbackThreadSyncObject) {
            // initialize flags:
            const resetOccurred: boolean = this.resetRequested;
            this.resetRequested = false;
            // const resetMetronomeBeatCounter: boolean = resetOccurred;
            // @ts-ignore
            const resetMetronomeBeatCounter: boolean = resetOccurred;
            let updateCursorPosition: boolean = resetOccurred;
            let endHasBeenReached: boolean = false;
            if (resetOccurred) {
                const shallPrecount: boolean = this.DoPreCount;
                this.doReset(shallPrecount);
            }

            if (this.musicPartManager === undefined) {
                return;
            }

            /**********************************************/
            // set the current values:
            this.currentTimestamp = this.timingSource.getCurrentTimestamp();
            // console.log("TS ms: " + this.timingSource.getCurrentTimeInMs());
            // console.log("TS ts: " + this.currentTimestamp);
            endHasBeenReached = this.cursorIterator.EndReached;

            /**********************************************/
            // handle the currently pending instructions:
            // stop the notes that are already over now:
            this.stopFinishedNotes();

            /***** process tempo instructions: *****/
            this.processTempoInstructions();

            if (this.RunningState === PlaybackState.Running) { // needed when resetting when in pause
                const newCursorTimestampReached: boolean = this.currentTimestamp.gte(this.cursorIterator.CurrentEnrolledTimestamp)
                    && !endHasBeenReached;
                if (newCursorTimestampReached) {

                    this.isPreCounting = false;

                    /***** Metronome Beat Calculations *****/
                    // check if the measure has changed:
                    if (this.currentMeasure !== this.cursorIterator.CurrentMeasure &&
                        this.cursorIterator.CurrentMeasure !== undefined) {
                        // set current measure to the new measure
                        this.currentMeasure = this.cursorIterator.CurrentMeasure;

                        this.startRhythmBeats = this.cursorIterator.currentPlaybackSettings().Rhythm.Numerator;
                        this.startRhythmDenominator = this.cursorIterator.currentPlaybackSettings().Rhythm.Denominator;

                        // get the enrolled timestamp of this measure start:
                        const relativeToMeasureTimestamp: Fraction = this.cursorIterator.CurrentRelativeInMeasureTimestamp;
                        this.currentMetronomeBaseTimestamp = Fraction.minus(this.cursorIterator.CurrentEnrolledTimestamp, relativeToMeasureTimestamp);
                        // calculate the new beat duration
                        this.currentBeatDuration = new Fraction(1, this.currentMeasure.Duration.Denominator);

                        // calculate which beat is next:
                        const relativeNextMetronomeBeatTimestamp: Fraction = new Fraction();
                        this.beatCounter = 0;
                        while (relativeNextMetronomeBeatTimestamp.lt(relativeToMeasureTimestamp)) {
                            relativeNextMetronomeBeatTimestamp.Add(this.currentBeatDuration);
                            this.beatCounter++;
                        }

                        this.playNextMetronomeAt = Fraction.plus(
                            this.currentMetronomeBaseTimestamp,
                            new Fraction(this.beatCounter, this.currentMeasure.Duration.Denominator)
                            );
                    }

                    /***** process dynamic instructions: *****/
                    const dynamicEntries: DynamicsContainer[] = this.cursorIterator.getCurrentDynamicChangingExpressions();
                    for (const dynamicEntry of dynamicEntries) {
                        const staff: Staff = this.musicPartManager.MusicSheet.getStaffFromIndex(dynamicEntry.staffNumber);
                        const channel: number =
                                this.instrumentToStaffToMidiChannelDict.getValue(staff);
                        //int channel = this.staffIndexToMidiChannelMapping[dynamicEntry.StaffNumber];
                        let volume: number = this.currentVolume;
                        if (dynamicEntry.parMultiExpression().StartingContinuousDynamic !== undefined) {
                            // dynamic expression is continuous:
                            const currentDynamicValue: number =
                                dynamicEntry.parMultiExpression().StartingContinuousDynamic.getInterpolatedDynamic(
                                    this.cursorIterator.CurrentSourceTimestamp);
                            if (currentDynamicValue >= 0) {
                                volume = this.calculateFinalVolume(currentDynamicValue);
                            }
                        } else { // dynamic Expression is instantanious - immediately set the volume:
                            volume = this.calculateFinalVolume(dynamicEntry.parMultiExpression().InstantaneousDynamic.Volume);
                        }

                        try {
                            if (this.audioPlayer !== undefined) {
                                this.audioPlayer.setVolume(channel, volume);
                            }
                        } catch (ex) {
                            log.info("PlaybackManager.handlePlaybackEvent: ", ex);
                        }
                    }

                    dynamicEntries.clear();
                }

                // check if the time has come to process the pending instructions:
                const dueEntries: { enrolledTimestamp: Fraction, playbackEntry: PlaybackEntry }[] = this.playbackIterator.Update(this.currentTimestamp);
                if (dueEntries.length > 0) {
                    // play new notes
                    if (this.DoPlayback) {
                        const playbackedNotes: PlaybackNote[] = [];

                        for (const entry of dueEntries) {
                            const playbackEntry: PlaybackEntry = entry.playbackEntry;
                            const voiceEntry: VoiceEntry = playbackEntry.ParentVoiceEntry;
                            if (playbackEntry.Notes.length === 0) {
                                continue;
                            }

                            const instrument: Instrument = voiceEntry.ParentVoice.Parent;
                            const staff: Staff = voiceEntry.Notes[0].ParentStaff;
                            const staffIndex: number =
                                MusicSheet.getIndexFromStaff(staff);
                            let channel: number = this.instrumentToStaffToMidiChannelDict.getValue(staff);
                            const isPercussion: boolean = instrument.MidiInstrumentId === MidiInstrument.Percussion;
                            // choose percussion channel if Selected
                            if (isPercussion) {
                                channel = this.percussionChannel;
                            }

                            const currentlyActiveExpression: AbstractExpression = this.cursorIterator.ActiveDynamicExpressions[staffIndex];

                            // adapt volume level for continuous expressions
                            if (currentlyActiveExpression instanceof ContinuousDynamicExpression) {
                                const currentDynamicValue: number =
                                    currentlyActiveExpression.getInterpolatedDynamic(
                                        this.cursorIterator.CurrentSourceTimestamp);
                                if (currentDynamicValue >= 0) {
                                    const channelVolume: number = this.calculateFinalVolume(currentDynamicValue);
                                    try {
                                        if (this.audioPlayer !== undefined) {
                                            this.audioPlayer.setVolume(channel, channelVolume);
                                        }
                                    } catch (ex) {
                                        log.info("PlaybackManager.handlePlaybackEvent: ", ex);
                                    }
                                }
                            }

                            // calculate volume from instrument volume, staff volume and voice volume:
                            let volume: number = instrument.Volume * staff.Volume * voiceEntry.ParentVoice.Volume;

                            // attenuate if in Solo mode an this voice is not soloed:
                            const soloAttenuate: boolean = this.SoloActive &&
                                !(instrument.Solo || voiceEntry.ParentVoice.Solo || staff.Solo);
                            if (soloAttenuate) {
                                volume *= this.SoloAttenuationValue;
                            }

                            // increase volume if this is an accent:
                            const entryIsAccent: boolean = voiceEntry.VolumeModifier !== undefined;
                            if (entryIsAccent) {
                                volume *= 1.3;
                                volume = Math.min(1, volume);
                            }

                            const transpose: number = this.musicPartManager.MusicSheet.Transpose;
                            const instrumentPlaybackTranspose: number = instrument.PlaybackTranspose ?? 0;

                            for (const note of playbackEntry.Notes.filter(n => n.MidiKey !== 0)) {
                                // play the note
                                let key: number = note.MidiKey;
                                if (!isPercussion) {
                                    key += instrumentPlaybackTranspose + transpose;
                                }

                                // if note has another explicitly given playback instrument:
                                if (note.ParentNote.PlaybackInstrumentId !== undefined) {
                                    // playback with other instrument:
                                    const notePlaybackInstrument: SubInstrument =
                                        instrument.getSubInstrument(note.ParentNote.PlaybackInstrumentId);
                                    if (notePlaybackInstrument !== undefined) {
                                        if (notePlaybackInstrument.fixedKey >= 0) {
                                            key = notePlaybackInstrument.fixedKey;
                                        }
                                    }

                                    // recalculate Volume for this instrument:
                                    volume = notePlaybackInstrument.volume * staff.Volume * voiceEntry.ParentVoice.Volume;

                                    // attenuate if in Solo mode an this voice is not soloed:
                                    if (soloAttenuate) {
                                        volume *= this.SoloAttenuationValue;
                                    }

                                    if (entryIsAccent) {
                                        volume *= 1.3;
                                        volume = Math.min(1, volume);
                                    }
                                }

                                // calculate stop time and remember it
                                let noteLength: Fraction = Fraction.createFromFraction(note.Length);
                                let stopAt: Fraction;

                                // ToDo MU: move this to PlaybackEntry
                                const entryIsStaccato: boolean = voiceEntry.DurationModifier !== undefined;
                                if (entryIsStaccato) {
                                    // Reduce length and stopAt time:
                                    noteLength = new Fraction(noteLength.Numerator * 2, noteLength.Denominator * 3);
                                    stopAt = Fraction.plus(entry.enrolledTimestamp, noteLength);
                                } else {
                                    stopAt = Fraction.plus(entry.enrolledTimestamp, noteLength);
                                }

                                try {
                                    if (this.audioPlayer !== undefined) {
                                        this.audioPlayer.playSound( channel,
                                                                    key,
                                                                    volume,
                                                                    this.timingSource.getDurationInMs(noteLength));
                                    }
                                } catch (ex) {
                                    log.info("PlaybackManager.handlePlaybackEvent. Failed playing sound: ", ex);
                                }

                                if (!this.notesToStop.containsKey(stopAt)) {
                                    // this.notesToStop.Add(stopAt, new List<ChannelNote>());
                                    this.notesToStop.setValue(stopAt, []);
                                }

                                this.notesToStop.getValue(stopAt).push(new ChannelNote(key, channel, note));

                                if (this.highlightPlayedNotes) {
                                    note.ParentNote.state = NoteState.Selected;
                                }
                                playbackedNotes.push(note);
                            }
                        }

                        /*** Inform about which notes are now played ***
                         * e.g. for updating graphics
                         */
                        // TODO: Replace with generic event system
                        // if (this.highlightPlayedNotes && this.NotesPlaybackEventOccurred !== undefined) {
                        this.NotesPlaybackEventOccurred(playbackedNotes);
                    }
                }

                if (newCursorTimestampReached) {
                    // store current iterator parameters:
                    this.currentIteratorSourceTimeStamp = this.cursorIterator.CurrentSourceTimestamp;
                    this.currentMeasureIndex = this.cursorIterator.CurrentMeasureIndex;
                    // this.currentRepetition = this.cursorIterator.CurrentRepetition;

                    /************ Move to next sheet position ************/
                    // move iterator already to next position, to find out how long to wait or if the End has been reached:
                    this.cursorIterator.moveToNext();
                    this.nextIteratorTimestamp = this.cursorIterator.CurrentEnrolledTimestamp;

                    updateCursorPosition = true;
                }

                // Stop the sound of the last played metronome
                this.stopMetronomeSound();

                // Check for "end has been reached"
                if (endHasBeenReached && this.currentTimestamp.gte(this.cursorIterator.CurrentEnrolledTimestamp)) {
                    // notify possible listeners:
                    for (const listener of this.listeners) {
                        listener.selectionEndReached(undefined);
                    }
                    this.handleEndReached();
                } else {
                    /******
                    * Play Metronome if needed
                    */
                    if (this.currentTimestamp.gte(this.playNextMetronomeAt)) {
                        updateCursorPosition = true;
                        const playFirstBeatSample: boolean = this.beatCounter % this.startRhythmBeats === 0;

                        this.playMetronomeSound(playFirstBeatSample);

                        this.beatCounter++;
                    }

                    // calculate the next metronome beat timestamp
                    if (this.currentMetronomeBaseTimestamp !== undefined) {
                        this.playNextMetronomeAt = Fraction.plus(
                            this.currentMetronomeBaseTimestamp,
                            new Fraction(this.beatCounter, this.startRhythmDenominator)
                            );
                    }

                    /*************************************/
                    this.calculateClosestNextTimestamp();
                }

            } else {
                // needed when a reset was requested: reset parameters, fire score position changed and finally stop again
                this.isPlaying = false;
            }

            // Check for "updating the display"
            if (updateCursorPosition ||
                endHasBeenReached && !this.loopTriggeredReset) {
                // set the play cursor in the display
                this.updateScoreCursorPosition(resetOccurred);
            }
        // }
    }

    private NotesPlaybackEventOccurred(notes: PlaybackNote[]): void {
        for (const listener of this.listeners) {
            listener.notesPlaybackEventOccurred(notes);
        }
    }

    public calculateFinalVolume(volume: number): number {
        return ((volume - this.defaultVolume) * this.dynamicImpactFactor + this.defaultVolume);
    }

    // TODO unused!?
    // @ts-ignore
    private loop(): void {
        // start playing:
        try {
            this.isRunning = true;
            // @ts-ignore
            const reset: boolean = false;

            if (this.isPlaying) {
                try {
                    if (this.isRunning && this.isInitialized) {

//console.log(`handlePlayback, timing deviation: ${Math.round(this.timingSource.getCurrentTimeInMs()) - Math.round(this.wantedNextIteratorTimestampMs)}`);

                        this.handlePlaybackEvent();

                        if (this.closestNextTimestamp !== undefined) {
                            const wantedNextElapsedMs: number = this.timingSource.getWaitingTimeForTimestampInMs(this.closestNextTimestamp);
                            //this.wantedNextIteratorTimestampMs = this.timingSource.getCurrentTimeInMs() + wantedNextElapsedMs;
                            window.setTimeout(() => { this.loop(); }, Math.max(0, wantedNextElapsedMs));
                            //this.interruptWaiting.WaitOne(Math.max(0, wantedNextElapsedMs));
                        }
                    }
                } catch (ex) {
                    this.pause();
                    this.reset();
                    const errorMsg: string = TextTranslation.translateText(
                        "MidiPlaybackError",
                        "An error occurred at the Midi Playback."
                        );
                    log.info("PlaybackManager.loop: " + errorMsg + " ", ex);
                    if (this.messageViewer !== undefined && this.messageViewer.MessageOccurred !== undefined) {
                        this.messageViewer.MessageOccurred(MessageBoxType.Error, errorMsg);
                    }
                }
            }
        } catch (ex) {
            const errorMsg: string = TextTranslation.translateText(
                "MidiPlaybackLoopError",
                "An error occurred at the Midi Playback. Please restart the program in order for the Playback to be availiable again."
                );
            log.info("PlaybackManager.loop: " + errorMsg + " ", ex);
            if (this.messageViewer !== undefined && this.messageViewer.MessageOccurred !== undefined) {
                this.messageViewer.MessageOccurred(MessageBoxType.Error, errorMsg);
            }
        }
        this.isRunning = false;
    }

    private stopAllCurrentlyPlayingNotes(): void {
        try {
            // lock(this.playbackThreadSyncObject) {
                if (this.audioPlayer !== undefined) {
                    // stop active metronome sound
                    this.audioPlayer.stopSound(this.metronomeNoteFirstBeat.channel, this.metronomeNoteFirstBeat.key);
                    this.audioPlayer.stopSound(this.metronomeNote.channel, this.metronomeNote.key);

                    // stop active notes
                    // TODO MB: check if port of following for..of is correct
                    // check same in for..of below
                    // for (const entry of this.notesToStop) {
                    //     for (const note of entry.Value) {
                    //         this.audioPlayer.stopSound(note.channel, note.key);
                    //     }
                    // }
                    for (const entry of this.notesToStop.values()) {
                        for (const note of entry) {
                            this.audioPlayer.stopSound(note.channel, note.key);
                        }
                    }
                }

                /*** Inform about which notes are now stopped ***
                * e.g. for updating graphics
                */
                const notes: Note[] = [];
                for (const entry of this.notesToStop.values()) {
                    for (const note of entry) {
                        note.note.ParentNote.state = NoteState.Normal;
                        notes.push(note.note.ParentNote);
                    }
                }

                if (this.highlightPlayedNotes) {
                    // TODO: Replace with generic even system
                    // if (this.NotesPlaybackEventOccurred !== undefined) {
                    //     this.NotesPlaybackEventOccurred(notes);
                    // }
                }
                this.notesToStop.clear();
            // }
        } catch (ex) {
            log.info("PlaybackManager.stoppAllCurrentlyPlayingNotes: ", ex);
        }
    }

    protected doReset(shallPrecount: boolean): void {
        this.nextIteratorTimestamp = undefined;
        this.playNextMetronomeAt = undefined;
        this.closestNextTimestamp = undefined;
        this.currentMeasure = undefined;
        this.beatCounter = 0;
        this.fermataActive = false;

        this.stopAllCurrentlyPlayingNotes();

        if (this.musicPartManager !== undefined) {
            this.cursorIterator = this.musicPartManager.getIterator();
        }

        if (this.cursorIterator === undefined) {
            return;
        }

        this.playbackIterator.Reset();
        this.currentIteratorSourceTimeStamp = this.cursorIterator.CurrentSourceTimestamp;
        this.nextIteratorTimestamp = this.cursorIterator.CurrentEnrolledTimestamp;

        this.currentMeasure = this.cursorIterator.CurrentMeasure;
        this.currentMeasureIndex = this.cursorIterator.CurrentMeasureIndex;
        // this.currentRepetition = this.cursorIterator.CurrentRepetition;

        this.startRhythmBeats = this.cursorIterator.currentPlaybackSettings().Rhythm.Numerator;
        this.startRhythmDenominator = this.cursorIterator.currentPlaybackSettings().Rhythm.Denominator;

        let preCountDuration: Fraction = new Fraction();
        if (shallPrecount) {
            this.isPreCounting = true;
            const rhythmDuration: Fraction = new Fraction(this.startRhythmBeats, this.startRhythmDenominator);
            const duration: Fraction =
                Fraction.plus(  this.musicPartManager.MusicSheet.SourceMeasures[this.currentMeasureIndex].AbsoluteTimestamp,
                                this.musicPartManager.MusicSheet.SourceMeasures[this.currentMeasureIndex].Duration).Sub(this.currentIteratorSourceTimeStamp);

            preCountDuration = rhythmDuration;

            if (rhythmDuration.gte(duration)) { // make sure that missing duration can't get negative (e.g. if measure is longer that given rhythm.
                const missingDuration: Fraction = Fraction.minus(rhythmDuration, duration);

                if (missingDuration.RealValue / rhythmDuration.RealValue < 0.5) {
                    preCountDuration.Add(missingDuration);
                } else {
                    preCountDuration = missingDuration;
                }
            }
        }

        this.currentMetronomeBaseTimestamp = this.playNextMetronomeAt = Fraction.minus(this.cursorIterator.CurrentEnrolledTimestamp, preCountDuration);
        //this.timingSource.Reset();
        this.timingSource.setTimeAndBpm(this.currentMetronomeBaseTimestamp,
                                        this.cursorIterator.currentPlaybackSettings().BeatsPerMinute);

        this.calculateClosestNextTimestamp();
    }

        /// <summary>
        /// Calculate the closest next timestamp at which the next instruction has to be processed
        /// </summary>
    private calculateClosestNextTimestamp(): void {
        const timestamps: Fraction[] = [];

        // add next timestamp for stopping notes
        if (this.notesToStop.size() > 0) {
            // timestamps.push(this.notesToStop.keys().Min());
            // TODO MB: Check if line below does what line above is supposed to do
            timestamps.push(this.notesToStop.keys().reduce( (a, b) => a.lt(b) ? a : b));
        }
        // add next timestamp for next notes or other sheet instruction
        if (this.playbackIterator.NextEntryTimestamp !== undefined) {
            timestamps.push(this.playbackIterator.NextEntryTimestamp);
        }
        if (this.nextIteratorTimestamp !== undefined) {
            timestamps.push(this.nextIteratorTimestamp);
        }
        // add next timestamp for metronome tick
        if (this.playNextMetronomeAt !== undefined) {
            timestamps.push(this.playNextMetronomeAt);
        }
        // get the closest next timestamp
        if (timestamps.length > 0) {
            // this.closestNextTimestamp = timestamps.Min();
            // TODO MB: Check if line below does what line above is supposed to do
            this.closestNextTimestamp = timestamps.reduce( (a, b) => a.lt(b) ? a : b);
        } else {
            this.closestNextTimestamp = undefined;
        }
    }

        /// <summary>
        /// Called when the end of the sheet or the selection has been reached
        /// </summary>
        protected handleEndReached(): void {
            this.pause();
        }

        /// <summary>
        /// Fire a delegate to inform the display, that the cursor position has changed
        /// </summary>
        /// <param name="resetOccurred"></param>
        private updateScoreCursorPosition(resetOccurred: boolean): void {
            this.scorePositionChangedData.CurrentMeasureIndex = this.currentMeasureIndex;
            this.scorePositionChangedData.CurrentRepetition = this.currentRepetition;
            this.scorePositionChangedData.PredictedPosition = this.currentTimestamp;
            this.scorePositionChangedData.CurrentBpm = this.musicPartManager.MusicSheet.SheetPlaybackSetting.BeatsPerMinute;
            this.scorePositionChangedData.ResetOccurred = resetOccurred;
            for (const listener of this.listeners) {
                listener.cursorPositionChanged(this.currentIteratorSourceTimeStamp, this.scorePositionChangedData);
            }
        }

        private stopMetronomeSound(): void {
            if (this.metronomeSoundPlayed) {
                if (this.audioPlayer !== undefined) {
                    this.audioPlayer.stopSound(this.metronomeNoteFirstBeat.channel, this.metronomeNoteFirstBeat.key);
                    this.audioPlayer.stopSound(this.metronomeNote.channel, this.metronomeNote.key);
                }
                this.metronomeSoundPlayed = false;
            }
        }

        private playMetronomeSound(playFirstBeatSample: boolean): void {
            // play the metronome if needed:
            if (this.metronome.Audible ||
                this.metronome.Solo ||
                this.isPreCounting) {
                let volume: number = this.metronome.Volume;
                if (!this.isPreCounting && this.SoloActive && !this.metronome.Solo) {
                    volume *= this.SoloAttenuationValue;
                }

                if (volume > 0) {
                    if (playFirstBeatSample) {
                        try {
                            if (this.audioPlayer !== undefined) {
                                this.audioPlayer.playSound(this.metronomeNoteFirstBeat.channel, this.metronomeNoteFirstBeat.key, volume, 1000);
                            }

                            if (this.audioMetronomePlayer !== undefined) {
                                this.audioMetronomePlayer.playFirstBeatSample(volume);
                            }
                        } catch (ex) {
                            log.info("PlaybackManager.playMetronomeSound: ", ex);
                        }
                    } else {
                        try {
                            if (this.audioPlayer !== undefined) {
                                this.audioPlayer.playSound(this.metronomeNote.channel, this.metronomeNote.key, volume, 1000);
                            }

                            if (this.audioMetronomePlayer !== undefined) {
                                this.audioMetronomePlayer.playBeatSample(volume);
                            }
                        } catch (ex) {
                            log.info("PlaybackManager.playMetronomeSound: ", ex);
                        }
                    }
                    this.metronomeSoundPlayed = true;
                }
            }
        }

        private stopFinishedNotes(): void {
            // do the pending note stops:
            let expiredKeys: Fraction[];

            if (this.currentTimestamp !== undefined) {
                expiredKeys = this.notesToStop.keys().filter(ts => ts.lte(this.currentTimestamp));
            } else {
                expiredKeys = this.notesToStop.keys();
            }

            for (const timestamp of expiredKeys) {
                const notesToStop: ChannelNote[] = this.notesToStop.getValue(timestamp);
                if (this.audioPlayer !== undefined) {
                    for (const note of notesToStop) {
                        this.audioPlayer.stopSound(note.channel, note.key);
                    }
                }

                /*** Inform about which notes are now stopped ***
                * e.g. for updating graphics
                */
                const notes: Note[] = [];
                for (const note of notesToStop) {
                    note.note.ParentNote.state = NoteState.Normal;
                    notes.push(note.note.ParentNote);
                }

                if (this.highlightPlayedNotes) {
                    // TODO: Replace with generic event system
                    // if (this.NotesPlaybackEventOccurred !== undefined) {
                    //     this.NotesPlaybackEventOccurred(notes);
                    // }
                }

                this.notesToStop.remove(timestamp);
            }
        }

    private processTempoInstructions(): void {
        // 1. check if the current bpm of the iterator have changed (significantly):
        if (Math.abs(this.currentReferenceBPM - this.cursorIterator.CurrentBpm) > 0.001) {
            this.changeTempo(this.cursorIterator.CurrentBpm);
        }

        // 2. check for possible fermatas and slow down for that entry:
        this.handleFermata();
    }

    private handleFermata(): void {
        // check for fermatas:
        let fermataFound: boolean = false;

        if (!this.cursorIterator.EndReached) {
            if (this.currentTimestamp.gte(this.cursorIterator.CurrentEnrolledTimestamp)) {
                for (const ve of this.cursorIterator.CurrentVoiceEntries) {
                    fermataFound = ve.Fermata !== undefined;
                }
            }
        }

        if (fermataFound) {
            if (!this.fermataActive) {
                this.fermataActive = true;
                this.changeTempo(this.cursorIterator.CurrentBpm / 3);
            }
        } else {
            if (this.fermataActive) {
                this.fermataActive = false;
                this.changeTempo(this.cursorIterator.CurrentBpm);
            }
        }
    }

    public bpmChanged(newBpm: number): void {
        this.currentBPM = newBpm;
        this.timingSource.setBpm(newBpm);
    }
    public volumeChanged(instrument: number, newVolume: number): void {
        this.currentVolume = newVolume / 100;
        if (instrument === -1) {
            this.metronome.Volume = this.currentVolume;
        } else {
            this.instrumentIdMapping.getValue(instrument).Volume = this.currentVolume;
        }
    }

    public volumeMute(instrument: number): void {
        if (instrument === -1) {
            this.metronome.Mute = true;
        } else {
            this.instrumentIdMapping.getValue(instrument).Audible = false;
        }
    }
    public volumeUnmute(instrument: number): void {
        if (instrument === -1) {
            this.metronome.Mute = false;
        } else {
            this.instrumentIdMapping.getValue(instrument).Audible = true;
        }
    }

    private changeTempo(newTempoInBPM: number): void {
        log.debug("PlaybackManager.changeTempo", `current tempo in BPM: ${newTempoInBPM}`);
        //Console.WriteLine(currTempoInBPM.ToString());

        if (newTempoInBPM > 0) {
            this.currentReferenceBPM = newTempoInBPM;
            this.setTempo();
        }
    }

    protected setTempo(): void {
        this.currentBPM = this.tempoUserFactor * this.getCurrentReferenceBPM();

        this.timingSource.setBpm(this.currentBPM);
    }

    protected getCurrentReferenceBPM(): number {
        return ((this.currentReferenceBPM - this.sheetStartBPM) * this.tempoImpactFactor + this.sheetStartBPM);
    }

    public checkForSoloDeactivated(): void {
        if (this.musicPartManager.MusicSheet === undefined) {
            this.SoloActive = false;
            return;
        }

        let state: boolean = false;
        for (const instrument of this.musicPartManager.MusicSheet.Instruments) {
            for (const staff of instrument.Staves) {
                state = state || staff.Solo;
            }
            for (const voice of instrument.Voices) {
                state = state || voice.Solo;
            }
        }

        state = state || this.Metronome.Solo;

        if (!state) {
            this.SoloActive = false;
        }
    }

        //private class MidiChannelInfo
        //{
        //    public List<IInstrument> subscribers = new List<IInstrument>();
        //    public int channel;
        //}

}
