
// clocks for timing threads.

Clock {
	// abstract class
	*play { | task | this.sched(0, task) }
	*seconds { ^thisThread.seconds }
	
	// tempo clock compatibility
	*beats { ^thisThread.seconds }
	*beats2secs { | beats | ^beats }
	*secs2beats { | secs | ^secs }
	*beats2bars { ^0 }
	*bars2beats { ^0 }
	*timeToNextBeat { ^0 }
	*nextTimeOnGrid { | quant = 1, phase = 0| 
		if (quant ==0) { ^this.beats + phase };
		if (phase < 0) { phase = phase % quant };
		^roundUp(this.beats - (phase % quant), quant) + phase;
	}
}

SystemClock : Clock {
	*clear {
		_SystemClock_Clear
		^this.primitiveFailed
	}
	*sched { arg delta, item;
		_SystemClock_Sched
		^this.primitiveFailed
	}
	*schedAbs { arg time, item;
		_SystemClock_SchedAbs
		^this.primitiveFailed
	}
		
}

AppClock : Clock {
	classvar scheduler;
	*initClass {
		scheduler = Scheduler.new(this, true);
	}
	*clear {
		scheduler.clear;
	}
	*sched { arg delta, item;
		scheduler.sched(delta, item)
	}
	*tick {
		var saveClock = thisThread.clock;
		thisThread.clock = this;
		scheduler.seconds = Main.elapsedTime;
		thisThread.clock = saveClock;
	}
	
}

Scheduler {
	var clock, drift, beats = 0.0, <seconds = 0.0, queue;
	
	*new { arg clock, drift = false;
		^super.newCopyArgs(clock, drift).init;
	}
	init {
		beats = thisThread.beats;
		queue = PriorityQueue.new;
	}

	play { arg task;
		this.sched(0, task)
	}

	schedAbs { | time, item |
		queue.put(time, item);
	}

	sched { | delta, item |
		var fromTime;
		if (delta.notNil, { 
			fromTime = if (drift, { Main.elapsedTime },{ seconds });
			queue.put(fromTime + delta, item);
		});
	}
	clear { // adc: priorityqueue has no pairsDo method, array has
		if(queue.array.notNil,{
			queue.array.pairsDo { | time, item | item.removedFromScheduler };
		});
		queue.clear 
	}

	isEmpty { ^queue.isEmpty }
	
	advance { | delta |
		this.seconds = seconds + delta;
	}
	
	seconds_ { | newSeconds  |
		var delta, item;
		while ({ 
			seconds = queue.topPriority; 
			seconds.notNil and: { seconds <= newSeconds }
		},{ 
			item = queue.pop;
			delta = item.awake( beats, seconds, clock );
			if (delta.isNumber, {
				this.sched(delta, item); 
			});
		});
		seconds = newSeconds;
		beats = clock.secs2beats(newSeconds);
	}
}

TempoClock : Clock {
	classvar <all, <>default;

	var <queue, ptr;
	
	var <beatsPerBar=4.0, barsPerBeat=0.25;
	var <baseBarBeat=0.0, <baseBar=0.0;
	var <>permanent=false;

/*
You should only change the tempo 'now'. You can't set the tempo at some beat in the future or past, even though you might think so from the methods. 

There are several ideas of now:
	elapsed time, i.e. "real time"
	logical time in the current time base.
	logical time in another time base.

logical time is time that is incremented by exact amounts from the time you started. It is not affected by the actual time your task gets scheduled, which may shift around somewhat due to system load. By calculating using logical time instead of actual time, your process will not drift out of sync over long periods. every thread stores a clock and its current logical time in seconds and beats relative to that clock.

elapsed time is whatever the system clock says it is right now. elapsed time is always advancing. logical time only advances when your task yields or returns.

*/

	*new { arg tempo, beats, seconds, queueSize=256;
		^super.new.init(tempo, beats, seconds, queueSize)
	}
	
	*initClass {
		default = this.new.permanent_(true);
		CmdPeriod.add(this);
	}	
	
	*cmdPeriod {
		all.do({ arg item; item.clear(false) });
		all.do({ arg item; if (item.permanent.not, { item.stop })  })
	}	
	
	init { arg tempo, beats, seconds, queueSize;
		queue = Array.new(queueSize);
		this.prStart(tempo, beats, seconds);
		all = all.add(this);
	}

	stop {
		this.changed(\stop);
		this.releaseDependants;
		all.take(this);
		this.prStop;
	}
	
	play { arg task, quant = 1; 
		this.schedAbs(quant.nextTimeOnGrid(this), task) 
	}
	
	playNextBar { arg task; this.schedAbs(this.nextBar, task) }
	
	tempo {
		_TempoClock_Tempo
		^this.primitiveFailed
	}
	beatDur {
		_TempoClock_BeatDur
		^this.primitiveFailed
	}
	elapsedBeats {
		_TempoClock_ElapsedBeats
		^this.primitiveFailed
		/* primitive does this:
			^this.secs2beats(Main.elapsedTime).
		*/
	}
	beats {
		// returns the appropriate beats for this clock from any thread
		_TempoClock_Beats
		^this.primitiveFailed
		/* primitive does this:
			if (thisThread.clock == this) { ^thisThread.beats }
			^this.secs2beats(thisThread.seconds)
		*/
	}

	seconds { ^thisThread.seconds }

	sched { arg delta, item;
		_TempoClock_Sched
		^this.primitiveFailed
	}
	schedAbs { arg beat, item;
		_TempoClock_SchedAbs
		^this.primitiveFailed
	}
	clear { | releaseNodes = true |
		// flag tells EventStreamPlayers that CmdPeriod is removing them, so
		// nodes are already freed
		queue.pairsDo { arg time, item; item.removedFromScheduler(releaseNodes) };
		^this.prClear;
	}

		
	// for setting the tempo at the current logical time 
	// (even another TempoClock's logical time).
	tempo_ { arg newTempo;
		this.setTempoAtBeat(newTempo, this.beats);
		this.changed(\tempo);  // this line is added
	}
	beatsPerBar_ { arg newBeatsPerBar;
		if (thisThread.clock != this) { 
			"should only change beatsPerBar within the scheduling thread.".error;
			^this
		};
		this.setMeterAtBeat(newBeatsPerBar, thisThread.beats);
	}

	// for setting the tempo at the current elapsed time .
	etempo_ { arg newTempo;
		this.setTempoAtSec(newTempo, Main.elapsedTime);
	}

	beats2secs { arg beats; 
		_TempoClock_BeatsToSecs
		^this.primitiveFailed
	}
	secs2beats { arg secs; 
		_TempoClock_SecsToBeats
		^this.primitiveFailed
	}
	
	prDump { 
		_TempoClock_Dump
		^this.primitiveFailed
	}
	
	nextTimeOnGrid { arg quant = 1, phase = 0;
		if (quant == 0) { ^this.beats + phase };
		if (quant < 0) { quant = beatsPerBar * quant.neg };
		if (phase < 0) { phase = phase % quant };
		^roundUp(this.beats - baseBarBeat - (phase % quant), quant) + baseBarBeat + phase
	}
	
	timeToNextBeat { arg quant=1.0; // logical time to next beat
		^quant.nextTimeOnGrid(this) - this.beats
	}
	
	beats2bars { arg beats;
		^(beats - baseBarBeat) * barsPerBeat + baseBar;
	}
	bars2beats { arg bars;
		^(bars - baseBar) * beatsPerBar + baseBarBeat;
	}
	bar {
		// return the current bar.
		^this.beats2bars(this.beats).floor;
	}
	nextBar { arg beat;
		// given a number of beats, determine number beats at the next bar line.
		if (beat.isNil) { beat = this.beats };
		^this.bars2beats(this.beats2bars(beat).ceil);
	}
	beatInBar { 
		// return the beat of the bar, range is 0 to < t.beatsPerBar
		^this.beats - this.bars2beats(this.bar) 
	}
		
	// PRIVATE
	prStart { arg tempo;
		_TempoClock_New
		^this.primitiveFailed
	}
	prStop {
		_TempoClock_Free
		^this.primitiveFailed
	}
	prClear {
		_TempoClock_Clear
		^this.primitiveFailed
	}
	setTempoAtBeat { arg newTempo, beats;
		_TempoClock_SetTempoAtBeat
		^this.primitiveFailed
	}
	setTempoAtSec { arg newTempo, secs;
		_TempoClock_SetTempoAtTime
		^this.primitiveFailed
	}
	// meter should only be changed in the TempoClock's thread.
	setMeterAtBeat { arg newBeatsPerBar, beats;
		// bar must be integer valued when meter changes or confusion results later.
		baseBar = round((beats - baseBarBeat) * barsPerBeat + baseBar, 1);
		baseBarBeat = beats;
		beatsPerBar = newBeatsPerBar;
		barsPerBeat = beatsPerBar.reciprocal;
		this.changed(\meter);
	}

// these methods allow TempoClock to act as TempoClock.default
   *stop { TempoClock.default.stop  }
   *play { | task, quant | TempoClock.default.play(task, quant)  }
   *sched { | delta, item | TempoClock.default.sched(delta, item)  }
   *schedAbs { | beat, item | TempoClock.default.schedAbs(beat, item)  }
   *clear { | releaseNodes | TempoClock.default.clear(releaseNodes)  }
   *tempo_ { | newTempo | TempoClock.default.tempo_(newTempo)  }
   *etempo_ { | newTempo | TempoClock.default.etempo_(newTempo)  }

   *tempo { ^TempoClock.default.tempo }
   *beats { ^TempoClock.default.beats }
   *beats2secs { | beats | ^TempoClock.default.beats2secs(beats)  }
   *secs2beats { | secs | ^TempoClock.default.secs2beats(secs)  }
   *nextTimeOnGrid { | quant, phase | ^TempoClock.default.nextTimeOnGrid(quant, phase)  }
   *timeToNextBeat { | quant | ^TempoClock.default.timeToNextBeat(quant)  }

   *setTempoAtBeat { | newTempo, beats | TempoClock.default.setTempoAtBeat(newTempo, beats)  }
   *setTempoAtSec { | newTempo, secs | TempoClock.default.setTempoAtSec(newTempo, secs)  }
   *setMeterAtBeat { | newBeatsPerBar, beats | TempoClock.default.setMeterAtBeat(newBeatsPerBar, beats)  }

   *beatsPerBar { ^TempoClock.default.beatsPerBar  }
   *baseBarBeat { ^TempoClock.default.baseBarBeat  }
   *baseBar { ^TempoClock.default.baseBar  }
   *playNextBar { | task | ^TempoClock.default.playNextBar(task)  }
   *beatDur { ^TempoClock.default.beatDur  }
   *elapsedBeats { ^TempoClock.default.elapsedBeats  }
   *beatsPerBar_ { | newBeatsPerBar | TempoClock.default.beatsPerBar_(newBeatsPerBar)  }
   *beats2bars { | beats | ^TempoClock.default.beats2bars(beats)  }
   *bars2beats { | bars | ^TempoClock.default.bars2beats(bars)  }
   *bar { ^TempoClock.default.bar  }
   *nextBar { | beat | ^TempoClock.default.nextBar  }
   *beatInBar { ^TempoClock.default.beatInBar  }

}

