from Csound import *

class TrackEvent:
    def __init__(self, sequencer, uniqueID, trackIndex, startTime, duration, noteNum, pFields = None):
        self.sequencer = sequencer
        self._trackIndex = trackIndex
        self._uniqueID = uniqueID
        self._startTime = startTime
        self._duration = duration
        self._noteNum = noteNum
        self._pFields = pFields

    def uniqueID(self):
        return self._uniqueID

    def trackIndex(self):
        return self._trackIndex
    def setTrackIndex(self, trackIndex):
        self._trackIndex = trackIndex
        
    def startTime(self):
        return self._startTime
    def setStartTime(self, startTime):
        self._startTime = startTime
        
    def duration(self):
        return self._duration
    def setDuration(self, duration):
        self._duration = duration

    def noteNum(self):
        return self._noteNum
    def setNoteNum(self, noteNum):
        self._noteNum = noteNum

    def pFields(self):
        return self._pFields
    def setPFields(self, pFields):
        self._pFields = pFields

    def requestUpdate(self, requestRef = 'EventRef'):

        message = OSC.OSCMessage()
        message.setAddress(self.sequencer.addressForInstance("/sequencer/update_event"))        
        message.append(requestRef)
        message.append(int(self._trackIndex))
        message.append(int(self._uniqueID))
        message.append(float(self._startTime))
        message.append(float(self._duration))
        message.append(float(self._noteNum))

        if self._pFields != None:
            for pField in self._pFields:
                message.append(float(pField))
            
        self.sequencer.sendMessage(message)        
        
    def __repr__(self):
        return "Event "+str(self._uniqueID)+" ["+str(self._noteNum)+"] ("+str(self._startTime)+", "+str(self._duration)+")"




class EventTrack:
    def __init__(self, sequencer, trackIndex):
        self._events = []
        self.sequencer = sequencer
        self.setIndex(trackIndex) 

    def __repr__(self):
        result = "Track ("+str(self._index)+")\n"
        for event in self._events:
            result += str(event) + "\n"
            
        return result

    def clear(self):
        self._events = []
        
    def setIndex(self, index):
        self._index = index

    def index(self):
        return self._index

    def eventById(self, id):
        for event in self._events:
            if event._uniqueID == id:
                return event
        
        return None

    def timeIntersectsEvent(self, time):
        for event in self._events:
            startTime = event.startTime()
            if time >= startTime and time < startTime + event.duration():
                return event

        return None

    def timeAndPitchIntersectsEvent(self, time, pitch):
        for event in self._events:
            startTime = event.startTime()
            if time >= startTime and time < startTime + event.duration():
                if pitch == event.noteNum():
                    return event

        return None
    
    def setInstrument(self, instrument):
        message = OSC.OSCMessage()
        message.setAddress(self.sequencer.addressForInstance("/sequencer/set_track_instrument"))        
        message.append(int(self._index))
        message.append(int(instrument))
        self.sequencer.sendMessage(message)
        
    def requestNewEvent(self, start, duration, notenum, pFields = None, requestRef = 'requestRef'):
        """
        Events are considered 'i' type line events
        consisting of start (p2), duration (p3) and note num (p4)
        and an optional list of additional p-fields
        
        the index determines whether the server has verified the addition
        of the track
        """

        if self.index >= 0:
            message = OSC.OSCMessage()
            message.setAddress(self.sequencer.addressForInstance("/sequencer/add_event"))        
            message.append(requestRef)
            message.append(int(self._index))
            message.append(float(start))
            message.append(float(duration))
            message.append(float(notenum))

            if pFields != None:
                for pField in pFields:
                    message.append(float(pField))
                
            self.sequencer.sendMessage(message)        

    def eventSorter(self, x, y):
        if x.startTime() > y.startTime():
            return 1
        elif x.startTime() == y.startTime():
            return 0
        else: 
            return -1            

    def addEvent(self, event):
        self._events.append(event)
        self.sort()
        
    def sort(self):
        self._events.sort(self.eventSorter)

    def events(self):
        return self._events

    def requestRemoveEvent(self, event, requestRef = 'removeEvent'):
        """
        Send a message to all servers asking them to remove the event
        A callback will be issued upon success
        """
        message = OSC.OSCMessage()
        message.setAddress(self.sequencer.addressForInstance("/sequencer/remove_event"))        
        message.append(requestRef)
        message.append(self._index)        
        message.append(event.uniqueID())
        self.sequencer.sendMessage(message)
        
    def removeEvent(self, event):
        self._events.remove(event) 


class CsoundEventSequencer:
    score = """
    f0 90000
    """

    def __init__(self, instanceName, orchestra):
        self.instanceName = instanceName
        self.orchestra = orchestra

        self.setDisplayMessages(True)
        
        self._tracks = {}
        self.localConnections = []
        self.remoteConnections = []
                
        self.timeCallback = None
                        
        self.trackAddedCallback = None
        self.trackRemovedCallback = None

        self.eventAddedCallback = None
        self.eventUpdateCallback = None
        self.eventRemovedCallback = None
        
        self.trackClearedCallback = None

        self.lastTime = 0
        
    def __repr__(self):
        result = "NetworkEventSequencer\n"        
        for track in self._tracks:
            result += str( self._tracks[track] ) + "\n"
        return result
        
    def shutdown(self):
        for csoundConnection in self.localConnections:
            csoundConnection.shutdown()
        for csoundConnection in self.remoteConnections:
            csoundConnection.shutdown()


    def shutdownServer(self):
        for csoundConnection in self.localConnections:
            csoundConnection.shutdownServer()
        for csoundConnection in self.remoteConnections:
            csoundConnection.shutdownServer()

    def setTempo(self, tempo):        
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/set_tempo"))        
        message.append(float(tempo))
        self.sendMessage(message)        

    def play(self):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/play"))
        self.sendMessage(message)        

    def pause(self):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/pause"))
        self.sendMessage(message)        

    def stop(self):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/stop"))
        self.sendMessage(message)        

            
    def track(self, index):
        return self._tracks[index]

    def tracks(self):
        return self._tracks

    def numTracks(self):
        return len( self._tracks )

    def setLoopPoints(self, start, duration):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/set_loop_points"))        
        message.append(float(start))
        message.append(float(duration))
        self.sendMessage(message)        

    def gotoBeat(self, beat):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/goto_beat"))        
        message.append(float(beat))
        self.sendMessage(message)        

    def setLoopEnabled(self, enabled):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/set_loop_enabled"))        
        message.append(int(enabled))
        self.sendMessage(message)        
        

    def addRemoteConnection(self, host, port, sampleRate = 44100):
        """
        Convenience function - reads better
        """
        return self.addConnection(host, port, False, sampleRate)

    def addLocalConnection(self, host, port, sampleRate = 44100):
        """ 
        Convenience function - reads better

        This will inform the server that we want to be notified to receive
        sequencer events from this server
        """
        return self.addConnection(host, port, True, sampleRate)
    
    def addConnection(self, host = 'localhost', port = 7000, wantsSequencerNotifications = True, sampleRate = 44100, controlRate = 441):
        try:
            csound = Csound(self.instanceName, host, port)            

            if wantsSequencerNotifications:
                csound.addCallback(self.eventTrackNotifications, "/sequencer", csound)
                
            csound.setMessageCallback(self.csoundMessages, csound)

            if csound.isInitialized() == 0:            
                csound.setupOrchestraAsString(self.orchestra)
                #csound.setupScoreAsString(self.score)
                csound.setSampleRate(sampleRate)
                csound.setControlRate(controlRate)                
                csound.initialize()
                csound.perform()
            
            if wantsSequencerNotifications:
                self.localConnections.append(csound)
            else:
                self.remoteConnections.append(csound)
                
            return csound
            
        except Exception, msg:
            print "Error adding connection to "+host+":"+str(port)
            print msg
    
            return None
        
    #
    # Callbacks
    #
    def csoundMessages(self, message):
        if self._displayMessages:
            csound = message.userData()
            message.display("cs: "+csound.host() + ":" + str(csound.port()))

    def setDisplayMessages(self, val):
        self._displayMessages = val
        
    def displayMessages(self):
        return self._displayMessages
        
    def eventTrackNotifications(self, message):
        """
        Receive notifications from the server regarding the sequence    
        """
        #csound = message.userData()
        #message.display(csound.host() + ":" + str(csound.port()) + ' EventTrackNotifications notification:')
        #print
        
        try:                
            if message.currentPath() == '/add_track':
                self.addTrackNotification(message)

            if message.currentPath() == '/clear_track':
                self.clearTrackNotification(message)

            if message.currentPath() == '/add_event':
                self.addEventNotification(message)

            if message.currentPath() == '/remove_event':
                self.removeEventNotification(message)

            if message.currentPath() == '/update_event':
                self.updateEventNotification(message)

            if message.currentPath() == '/beatsync':
                self.beatSync(message)
        
        except Exception, msg:
            print msg

    def beatSync(self, message):            
        #print message.argument(0)
        curTime = time.time()
        deltaTime = curTime - self.lastTime
        
        print deltaTime
        
        self.lastTime = curTime
        
        
    def setTrackAddedCallback(self, trackAddedCallback, userData = None):
        self.trackAddedCallback = [ trackAddedCallback, userData ]

    def setTrackRemovedCallback(self, trackRemovedCallback, userData = None):
        self.trackRemovedCallback = [ trackRemovedCallback, userData ]

    def setEventAddedCallback(self, eventAddedCallback, userData = None):
        self.eventAddedCallback = [ eventAddedCallback, userData ]

    def setEventRemovedCallback(self, eventRemovedCallback, userData = None):
        self.eventRemovedCallback = [ eventRemovedCallback, userData ]

    def setEventUpdateCallback(self, eventUpdateCallback, userData = None):
        self.eventUpdateCallback = [ eventUpdateCallback, userData ]

    def setTrackClearedCallback(self, trackClearedCallback, userData = None):
        self.trackClearedCallback = [ trackClearedCallback, userData ]


        
        
    def addTrackNotification(self, message):
        """
        the result of an addTrack request
        """
        # An add track was requested
        requestRef = message.argument(0)
        result = int( message.argument(1) )
        
        if result >= 0:
            if result in self._tracks:
                raise Exception("Request to add track that already exists : "+requestRef)            
            else:
                # add the track to our dict
                track = EventTrack(self, result)
                self._tracks[result] = track
                
                if self.trackAddedCallback != None:
                    self.trackAddedCallback[0](self, track, requestRef, self.trackAddedCallback[1])

    def removeEventNotification(self, message):
        """
        the result of a remove event request
        """
        # An add track was requested
        requestRef = message.argument(0)
        trackIndex = int( message.argument(1) )
        eventIdent = int( message.argument(2) )
        
        if eventIdent >= 0:
            track = self.track(trackIndex)
            
            event = track.eventById(eventIdent)
            track.removeEvent(event)
            
            if self.eventRemovedCallback != None:
                self.eventRemovedCallback[0](self, track, requestRef, self.eventRemovedCallback[1])

    def clearTrackNotification(self, message):
        trackIndex = message.argument(0)
        
        track = self._tracks[trackIndex]
        track.clear()

        if self.trackClearedCallback != None:
            self.trackClearedCallback[0](self, track, self.trackClearedCallback[1])
                    
    def addEventNotification(self, message):
        """
        the result of an addEvent request
        """
        # An add track was requested
        requestRef = message.argument(0)
        uniqueID = int( message.argument(1) )
        
        if uniqueID >= 0:

            trackIndex = message.argument(2)
            startTime = message.argument(3)
            duration = message.argument(4)
            noteNum = message.argument(5)
            pFields = message.argumentsFromOffset(6)
                        
            track = self._tracks[trackIndex]
            event = TrackEvent(self, uniqueID, trackIndex, startTime, duration, noteNum, pFields)
            track.addEvent(event)
            
            if self.eventAddedCallback != None:
                self.eventAddedCallback[0](self, track, event, requestRef, self.eventAddedCallback[1])

    def updateEventNotification(self, message):
        """
        the result of an updateEvent request
        """
        # An add track was requested
        requestRef = message.argument(0)
        uniqueID = int( message.argument(1) )
        
        if uniqueID >= 0:

            trackIndex = message.argument(2)
            startTime = message.argument(3)
            duration = message.argument(4)
            noteNum = message.argument(5)
            pFields = message.argumentsFromOffset(6)
            
            track = self._tracks[trackIndex]
            event = track.eventById(uniqueID)
            
            # make the modifications
            event.setStartTime(startTime)
            event.setDuration(duration)
            event.setNoteNum(noteNum)
            event.setPFields(pFields)
            
            track.sort()
                                                                                    
            if self.eventUpdateCallback != None:
                self.eventUpdateCallback[0](self, track, event, requestRef, self.eventUpdateCallback[1])

    def addressForInstance(self, path):
        """
        path - the path you want the instance to receive
        Constructs an appropriate address for the message based on the instance name
        """
        return "/" + self.instanceName + path

    def requestNewTrack(self, requestRef = 'requestRef'):
        """
        request a new track from the server
        we will be notified if this request was successful
        """
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/add_track"))        
        message.append(requestRef)
        
        self.sendMessage(message)

    def clearTrack(self, index):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/clear_track"))        
        message.append(index)
        
        self.sendMessage(message)
    
    def muteTrack(self, trackIndex):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/mute"))
        message.append(int(trackIndex))
        message.append(1)        
        self.sendMessage(message)

    def unmuteTrack(self, trackIndex):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/mute"))
        message.append(int(trackIndex))
        message.append(0)        
        self.sendMessage(message)

    def soloTrack(self, trackIndex):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/solo"))
        message.append(int(trackIndex))
        message.append(1)        
        self.sendMessage(message)

    def unsoloTrack(self, trackIndex):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/solo"))
        message.append(int(trackIndex))
        message.append(0)        
        self.sendMessage(message)
        

    def writeValueToChannel(self, channelName, value):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/channel"))
        message.append(channelName)
        message.append(float(value))        
        self.sendMessage(message)

    def setSyncMode(self, mode):
        message = OSC.OSCMessage()
        message.setAddress(self.addressForInstance("/sequencer/set_sync_mode"))
        message.append(int(mode))
        self.sendMessage(message)
        
    def playEvent(self, type, pfields):
        for csoundConnection in self.localConnections:
            csoundConnection.playEvent(type, pfields)
        for csoundConnection in self.remoteConnections:
            csoundConnection.playEvent(type, pfields)

    def sendMessage(self, message):
        """
        Broadcast message to all connections
        """
        for csoundConnection in self.localConnections:
            csoundConnection.messageIO.sendMessage(message)
        for csoundConnection in self.remoteConnections:
            csoundConnection.messageIO.sendMessage(message)
