"""

================================================================================

        mingus - Music theory Python package, general purpose sequencer
	Copyright (C) 2008-2009, Bart Spaans

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

================================================================================

    A general purpose sequencer for the objects in mingus.containers. You 
    can use the Sequencer object either by creating a subclass and implementing
    some of the events (`init`, `play_event`, `stop_event`, `cc_event`, `instr_event`)
    or by attaching observer objects via `attach` and catching the 
    messages in the notify(msg_type, param_dict) function of your object.
    See SequencerObserver for a pre made, easy to extend base class that 
    can be attached to the Sequencer.

================================================================================

"""


from datetime import datetime
from mingus.containers.Instrument import MidiInstrument
import pyFluidSynth as fs
from time import sleep

class Sequencer:
	"""A general purpose sequencer for the objects in mingus.containers. You \
can use the Sequencer object either by creating a subclass and implementing \
some of the events (`init`, `play_event`, `stop_event`, `cc_event`, `instr_event`) \
or by attaching observer objects via `attach` and catching the \
messages in the notify(msg_type, param_dict) function of your object. \
See SequencerObserver for a pre made, easy to extend base class that \
can be attached to the Sequencer."""

	output = None

        # Low level messages
        MSG_PLAY_INT = 0
        MSG_STOP_INT = 1
        MSG_CC = 2
        MSG_INSTR = 3

        # High level messages
        MSG_PLAY_NOTE = 4
        MSG_STOP_NOTE = 5
        MSG_PLAY_NC = 6
        MSG_STOP_NC = 7
        MSG_PLAY_BAR = 8
        MSG_PLAY_BARS = 9
        MSG_PLAY_TRACK = 10
        MSG_PLAY_TRACKS = 11
        MSG_PLAY_COMPOSITION = 12

        def __init__(self):
                self.listeners = []
                self.init()

        # Events
        # Implement some of these functions when subclassing

        def init(self):
                pass

        def play_event(self, note, channel,  velocity):
                pass

        def stop_event(self, channel, note):
                pass

        def cc_event(self, channel, control, value):
                pass

        def instr_event(self, channel, instr, bank):
                pass



        # Observer pattern
        # Attach any object that has a notify function

        def attach(self, listener):
                """Attach an object that should be notified of events. \
The object should have a notify(msg_type, param_dict) function."""
                if listener not in self.listeners:
                        self.listeners.append(listener)

        def detach(self, listener):
                """Detach a listening object so that it won't receive any \
events anymore."""
                if listener in self.listeners:
                        self.listeners.remove(listener)

        def notify_listeners(self, msg_type, params):
                """Send a message to all the observers."""
                for c in self.listeners:
                        c.notify(msg_type, params)




        def set_instrument(self, channel, instr, bank = 0):
		"""Sets the channel to the instrument _instr_."""
                self.instr_event(channel, instr, bank)
                self.notify_listeners(self.MSG_INSTR, {'channel' : int(channel),
                                             'instr': int(instr),
                                             'bank': int(bank) } )

        def control_change(self, channel, control, value):
                """Sends a control change message. \
See the MIDI specification for more information."""
		if control < 0 or control > 128:
			return False
		if value < 0 or value > 128:
			return False
                self.cc_event(channel, control, value)
                self.notify_listeners(self.MSG_CC, {'channel': int(channel),
                                          'control': int(control),
                                          'value' : int(value) } )
		return True

	def play_Note(self, note, channel = 1, velocity = 100):
		"""Plays a Note object on a channel with a \
velocity[0-127]. You can either specify the velocity and channel \
here as arguments or you can set the Note.velocity and Note.channel \
attributes, which will take presedence over the function arguments."""
		if hasattr(note, 'velocity'):
			velocity = note.velocity
		if hasattr(note, 'channel'):
			channel = note.channel
                self.play_event(int(note) + 12, int(channel), int(velocity))
                self.notify_listeners(self.MSG_PLAY_INT, {'channel': int(channel), 
                                            'note': int(note) + 12, 
                                            'velocity': int(velocity)} )
                self.notify_listeners(self.MSG_PLAY_NOTE, {'channel': int(channel), 
                                            'note': note, 
                                            'velocity': int(velocity)} )
		return True

	def stop_Note(self, note, channel = 1):
		"""Stops a note on a channel. If Note.channel is set, it \
will take presedence over the channel argument given here."""
		if hasattr(note, 'channel'):
			channel = note.channel

                self.stop_event(int(note) + 12, int(channel))
                self.notify_listeners(self.MSG_STOP_INT, {'channel': int(channel),
                                            'note': int(note) + 12})
                self.notify_listeners(self.MSG_STOP_NOTE, {'channel': int(channel),
                                            'note': note})
		return True

	def stop_everything(self):
		"""Stops all the notes on all channels."""
		for x in range(118):
			for c in range(16):
				self.stop_Note(x, c)


	def play_NoteContainer(self, nc, channel = 1, velocity = 100):
		"""Plays the Notes in the NoteContainer nc."""
                self.notify_listeners(self.MSG_PLAY_NC, {'notes': nc, 'channel': channel,
                                                         'velocity': velocity})
                if nc is None:
                        return True

		for note in nc:
			if not self.play_Note(note, channel, velocity):
				return False
		return True



	def stop_NoteContainer(self, nc, channel = 1):
		"""Stops playing the notes in NoteContainer nc."""
                self.notify_listeners(self.MSG_PLAY_NC, {'notes': nc, 'channel': channel})

                if nc is None:
                        return True
                
		for note in nc:
			if not self.stop_Note(note, channel):
				return False
		return True



	def play_Bar(self, bar, channel = 1, bpm = 120):
		"""Plays a Bar object. The tempo can be changed by setting \
the bpm attribute on a NoteContainer. Returns a dictionary with \
the bpm lemma set on success, an empty dict on some kind of failure. """

                self.notify_listeners(self.MSG_PLAY_BAR, {'bar': bar, 'channel': channel,
                                                          'bpm': bpm})
		# length of a quarter note
		qn_length = 60.0 / bpm

		for nc in bar:

			if not self.play_NoteContainer(nc[2], channel, 100):
				return {}

			# Change the quarter note length if the NoteContainer has a bpm attribute
			if hasattr(nc[2], "bpm"):
				bpm = nc[2].bpm
				qn_length = 60.0 / bpm
			
			sleep(qn_length * (4.0 / nc[1]))
			self.stop_NoteContainer(nc[2], channel)

		return {"bpm": bpm}


	def play_Bars(self, bars, channels, bpm = 120):
		"""Plays several bars (a list of Bar objects) at the same time. A list of \
channels should also be provided. The tempo can be changed by providing one or more of \
the NoteContainers with a bpm argument."""

                self.notify_listeners(self.MSG_PLAY_BARS, {'bars': bars, 'channels': channels,
                                                          'bpm': bpm})

		qn_length = 60.0 / bpm  # length of a quarter note
		tick = 0.0              # place in beat from 0.0 to bar.length
		cur = [0] * len(bars)   # keeps the index of the NoteContainer under investigation in each of the bars
		playing = []            # The NoteContainers being played.


		# The following sequencer is pretty complicated, but that's because we're playing
		# multiple bars at the same time, which in turn can consist of different note values.
		# Getting the timing right is the crucial task and most of the code here is
		# for finding out how long we should sleep between notes. 
		while tick < bars[0].length:

			# Prepare a and play a list of NoteContainers that are ready for it.
			# The list `playing_new` holds both the duration and the NoteContainer.
			playing_new = []
			for n, x in enumerate(cur):
				start_tick, note_length, nc = bars[n][x]
				if start_tick <= tick:
					self.play_NoteContainer(nc, channels[n])
					playing_new.append([note_length, n])
					playing.append([note_length, nc, channels[n], n])

					# Change the length of a quarter note if the NoteContainer has a bpm attribute
					if hasattr(nc, "bpm"):
						bpm = nc.bpm
						qn_length = 60.0 / bpm

			# Sort the list and sleep for the shortest duration
			if len(playing_new) != 0:
				playing_new.sort()
				shortest = playing_new[-1][0]
				sleep(qn_length * (4.0 / shortest))

			# If somehow, playing_new doesn't contain any notes
			# (something that shouldn't happen when the bar was filled
			# properly), we make sure that at least the notes that are 
			# still playing get handled correctly. 
			else:
				if len(playing) != 0:
					playing.sort()
					shortest = playing[-1][0]
					sleep(qn_length * (4.0 / shortest))
				else:
					#warning: this could lead to some strange behaviour.
					# OTOH. Leaving gaps is not the way Bar works.
					# should we do an integrity check on bars first?
					return {}

			# Add shortest interval to tick
			tick += 1.0 / shortest

			# This final piece adjusts the duration in `playing` and
			# checks if a NoteContainer should be stopped.
			new_playing = []
			for length, nc, chan, n in playing:
				duration = 1.0 / length - 1.0 / shortest
				if duration >= 0.00001:
					new_playing.append([1.0 / duration, nc, chan, n])
				else:
					self.stop_NoteContainer(nc, chan)
					if cur[n] < len(bars[n]) - 1:
						cur[n] += 1
			playing = new_playing

			
		# Stop all the notes that are still playing
		for p in playing:
			self.stop_NoteContainer(p[1], p[2])
			playing.remove(p)

		return {"bpm": bpm}


	def play_Track(self, track, channel = 1, bpm = 120):
		"""Plays a Track object."""
                self.notify_listeners(self.MSG_PLAY_TRACK, {'track': track, 'channel': channel,
                                                          'bpm': bpm})
		for bar in track:

			res = self.play_Bar(bar, channel, bpm)
			if res != {}:
				bpm = res["bpm"]
			else:
				return {}
		return {"bpm": bpm}

	def play_Tracks(self, tracks, channels, bpm =120):
		"""Plays a list of Tracks. If an instance of MidiInstrument is used \
then the instrument will be set automatically."""
		
                self.notify_listeners(self.MSG_PLAY_TRACKS, {'tracks': tracks, 'channels': channels,
                                                          'bpm': bpm})
		# Set the right instruments
		for x in range(len(tracks)):
			instr = tracks[x].instrument
			if isinstance(instr, MidiInstrument):
				try: 
					i = instr.names.index(instr.name)
				except:
					i = 1
				self.set_instrument(channels[x], i)
			else:
				self.set_instrument(channels[x], 1)

		
		current_bar = 0
		max_bar = len(tracks[0])

		# Play the bars
		while current_bar < max_bar:
			playbars = []
			for tr in tracks:
				playbars.append(tr[current_bar])
			res = self.play_Bars(playbars, channels, bpm)
			if res != {}:
				bpm = res["bpm"]
			else:
				return {}
			current_bar += 1

		return {"bpm":bpm}
			

	def play_Composition(self, composition, channels = None, bpm = 120):
		"""Plays a Composition object."""
                self.notify_listeners(self.MSG_PLAY_COMPOSITION, {'composition': composition, 
                                                                  'channels': channels, 'bpm': bpm})

		if channels == None:
			channels = map(lambda x: x + 1, range(len(composition.tracks)))
		return self.play_Tracks(composition.tracks, channels, bpm)


	## MIDI CONTROLLER TYPE 'SHORTCUTS'

	def modulation(self, channel, value):
		"""Sets the modulation"""
		return self.control_change(channel, 1, value)

	def main_volume(self, channel, value):
		"""Sets the main volume."""
		return self.control_change(channel, 7, value)

	def pan(self, channel, value):
		"""Sets the panning."""
		return self.control_change(channel, 10, value)

