import OSC
from MessageIO import *
import time
import os

class Csound:
    """
    Low-level interface for communicating with the Csound server directly.
    """

    def __init__(self, instanceName = None, host = 'localhost', port = 7000):

        #####################
        #  API VERSION INFO #
        self.majorVersion = 1
        self.minorVersion = 0
        #####################
        
        self.instanceName = ''
        self.messageIO = None

        self.isConnected = 0
        self.serverMajorVersion = -1
        self.serverMinorVersion = -1

        self.setSampleRate(44100)
        self.setControlRate(441)
    
        try:
            self.dispatcher = OSC.Dispatcher()            
            self.dispatcher.addToResponseWaitList("/connection")
            self.dispatcher.add(self.serverErrors, '/server/error')
                        
            self.messageIO = MessageIO(self.messageHandler)
            self.messageIO.connect(host, port)
            
            if self.waitForConnection() == 1:
                
                if instanceName != None:
                    self.createInstance(instanceName)
                    self.setupDisplaysEnabled(False)
                    
                    
                #self.setServerCallback(self.serverMessages)

        except Exception, msg:
            print "Exception occurred while initializing Csound ",
            if instanceName:
                print "("+instanceName+")",
            print host + ":" + str(port)
            print msg


    def waitForConnection(self):        
        """
        wait for connection to server and set the version info
        """    
        try:
            message = self.waitForResponse("/connection")
            
            self.isConnected = message.argument(0)
            self.serverMajorVersion = message.argument(1) 
            self.serverMinorVersion = message.argument(2)

            return self.isConnected

        except Exception, msg:
            print msg
            return -1
    
    def serverErrors(self, message):
        """
        Server error messages will be sent here
        """
        message.display("Server Error:")

    def setInstanceName(self, instanceName):
        """
        Set the name of the Csound instance you want to control
        """
        self.instanceName = instanceName
    
    def host(self):
        """
        Request the host
        """
        return self.messageIO.host

    def port(self):
        """
        Request the port
        """
        return self.messageIO.port

    def sampleRate(self):
        """
        get the instance sample rate
        """
        # register before we send the message
        self.dispatcher.addToResponseWaitList("/sample_rate")

        # send the message
        message = OSC.OSCMessage()
        message.setAddress(self.address("/sample_rate"))
        self.messageIO.sendMessage(message)    
        
        try:
            result = self.waitForResponse("/sample_rate")
            return result.argument(0)

        except Exception, msg:
            print msg
            return -1

    def controlRate(self):
        """
        get the instance sample rate
        """
        # register before we send the message
        self.dispatcher.addToResponseWaitList("/control_rate")

        # send the message
        message = OSC.OSCMessage()
        message.setAddress(self.address("/control_rate"))
        self.messageIO.sendMessage(message)    
        
        try:
            result = self.waitForResponse("/control_rate")
            return result.argument(0)

        except Exception, msg:
            print msg
            return -1
        
    def addCommandLineArgument(self, flag):
        """
        Add command line argument 
        """
        message = OSC.OSCMessage()
        message.setAddress(self.address("/setup/add_argument"))        
        message.append(flag)
        self.messageIO.sendMessage(message)                
    
    def serverMessages(self, message):
        """
        Messages from the server
        """
        message.display("server:")
        
    def setMessageCallback(self, method, userData = None):
        """
        Registers generic Csound messages
        """
        self.addCallback(method, "/csound", userData)

    def setServerCallback(self, method, userData = None):
        """
        Registers to receive messages from the server (e.g. errors)
        """
        self.addCallback(method, "/server", userData)

    def addCallback(self, method, oscAddress, userData = None):
        """
        method - the callback method
        oscAddress - the OSC path (e.g.  /my/path)
        userData - anything you want to be passed to the callback
        
        Registers an address or partial address to receive
        messages coming from the server that match the address
        """
        self.dispatcher.add(method, oscAddress, userData)

    def registerChannel(self, channelName, callbackMethod, userData = None):
        """
        register to receive values from a channel        
        """
        self.dispatcher.add(callbackMethod, "/channel/"+channelName, userData)        
        
    def messageHandler(self, message):
        """
        The messageHandler will accept messages from our MessageIO instance
        and then route them.
        """
        self.dispatcher.handle(message)

    def registerTime(self, callback, intervalInSecs, userData = None):
        """
        Register to receive current performance time notifications at a given interval
        """
        self.addCallback(callback, "/notification/time", userData)

        message = OSC.OSCMessage()
        message.setAddress(self.address("/register/time"))        
        message.append(float(intervalInSecs))
        self.messageIO.sendMessage(message)        
        
    
    def createInstance(self, instanceName):
        """
        Asks the server to create a new instance of Csound
        """
        self.instanceName = instanceName
    
        message = OSC.OSCMessage()
        message.setAddress("/new")
        message.append(self.instanceName)        
        self.messageIO.sendMessage(message)

    def address(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 waitForResponse(self, address, timeout = 2, waitTime = 0.1, okayToRemove = True):
        """
        Wait a specified amount of time for a response from the server
        address - the osc path to listen for
        timeout - the total time to wait (use -1 for infinite) 
        waitTime - the interval to test
        okayToRemove - flag to remove from the wait list when done
        """
        gotResponse = 0		
        totalWaitTime = 0

        if timeout > 0:        
            while totalWaitTime < timeout and gotResponse == 0:
                #print "waiting...",str(totalWaitTime)
                # check to see if we got a response
                if self.dispatcher.hasResponse(address):
                    #print "has response"
                    response = self.dispatcher.responseForAddress(address)                
                    gotResponse = 1

                # wait a bit before checking again   
                time.sleep(waitTime)
                totalWaitTime += waitTime
        else:
            while gotResponse == 0:
                #print "waiting...",str(totalWaitTime)
                # check to see if we got a response
                if self.dispatcher.hasResponse(address):
                    #print "has response"
                    response = self.dispatcher.responseForAddress(address)                
                    gotResponse = 1

                # wait a bit before checking again   
                time.sleep(waitTime)
        
        # we're done testing, so we can remove this now
        if okayToRemove:
            self.dispatcher.removeFromResponseWaitList(address)

        if gotResponse == 0:
            # we did not receive a response
            raise Exception('Timed out on ' + address)

        return response
        
    def isInitialized(self, timeout = 10):
        """
        Determine if the instance is already initialized (i.e., performing)
        """
        
        # register before we send the message
        self.dispatcher.addToResponseWaitList("/setup/is_initialized")

        # send the message
        message = OSC.OSCMessage()
        message.setAddress(self.address("/setup/is_initialized"))
        self.messageIO.sendMessage(message)    
        
        try:
            result = self.waitForResponse("/setup/is_initialized", timeout)
            return result.argument(0)

        except Exception, msg:
            print msg
            return -1

        
    def setupMessageLevel(self, messageLevel):
        """
        sets the instance's message level
        this can be a useful debugging tool
        Sum of:
                1=note amps, 2=out-of-range msg, 4=warnings
                0/32/64/96=note amp format (raw,dB,colors)
                128=print benchmark information
        """
        message = OSC.OSCMessage()
        message.setAddress(self.address("/setup/message_level"))        
        message.append(messageLevel)
        self.messageIO.sendMessage(message)    

    def setupDisplaysEnabled(self, status):
        """
        sets whether graphic displays will be generated
        status - True or False
        """
        if status == True:
            val = 1
        else:
            val = 0

        message = OSC.OSCMessage()
        message.setAddress(self.address("/setup/displays_enabled"))        
        message.append(val)
        self.messageIO.sendMessage(message)    
    
    def setupOrchestraFile(self, orcFile, expandPath = True, orcRefName = "orcref"):
        """ 
        orcFile - the file on the server (relative to where the server is running)
        expandPath - determines whether the path is expanded to the script's dir
        orcRefName - can be anything - reserved for making references to the orchestra
        """
        if expandPath:
            # TODO: replace os.getcwd() with a more reliable path function
            orcFile = os.getcwd() + '/' + orcFile        
        
        message = OSC.OSCMessage()
        message.setAddress(self.address("/setup/orchestra_file"))        
        message.append(orcRefName)
        message.append(orcFile)
        self.messageIO.sendMessage(message)

    def setupOrchestraAsString(self, orcString, orcRefName = "orcref"):
        """ 
        orcString - orchestra as a string (will be sent over the socket)
        orcRefName - can be anything - reserved for making references to the orchestra
        """

        message = OSC.OSCMessage()
        message.setAddress(self.address("/setup/orchestra_string"))        
        message.append(orcRefName)
        message.append(orcString)
        self.messageIO.sendMessage(message)

    def setupScoreFile(self, scoFile, expandPath = True, scoRefName = "scoref"):
        """ 
        scoFile - the file on the server (relative to where the server is running)
        expandPath - determines whether the path is expanded to the script's dir
        scoRefName - can be anything - reserved for making references to the score
        """
        if expandPath:
            # TODO: replace os.getcwd() with a more reliable path function
            scoFile = os.getcwd() + '/' + scoFile
        
        message = OSC.OSCMessage()
        message.setAddress(self.address("/setup/score_file"))        
        message.append(scoRefName)
        message.append(scoFile)
        self.messageIO.sendMessage(message)

    def setupScoreAsString(self, scoString, scoRefName = "scoref"):
        """ 
        scoString - precomposed score as a string (will be sent over the socket)
        scoRefName - can be anything - reserved for making references to the score
        """
        message = OSC.OSCMessage()
        message.setAddress(self.address("/setup/score_string"))        
        message.append(scoRefName)
        message.append(scoString)
        self.messageIO.sendMessage(message)

    def setupMIDIFile(self, midFile, expandPath = True, midRefName = "midref"):
        """ 
        midFile - the file on the server (relative to where the server is running)
        expandPath - determines whether the path is expanded to the script's dir
        midRefName - can be anything - reserved for making references to the score
        """
        if expandPath:
            # TODO: replace os.getcwd() with a more reliable path function
            midFile = os.getcwd() + '/' + midFile

        message = OSC.OSCMessage()
        message.setAddress(self.address("/setup/midi_file"))        
        message.append(midRefName)
        message.append(midFile)
        self.messageIO.sendMessage(message)

    def setControlRate(self, cr):
        """
        When used prior to an initialize call - this will override
        the default contorl rate        
        """
        self._controlRate = cr

    def setServerSampleRate(self, sr):
        """
        Set the sample-rate of the server (this must be before instances begin performing)
        """
        message = OSC.OSCMessage()
        message.setAddress("/set_sample_rate")
        message.append(int(sr))
        self.messageIO.sendMessage(message)
    
    def setSampleRate(self, sr):
        """
        When used prior to an initialize call - this will override
        the default sample rate        
        """
        self._sampleRate = sr

    def setupRealtimeMIDI(self):
        """
        Enable realtime MIDI control
        """
        self.addCommandLineArgument("-+rtmidi=null")
        self.addCommandLineArgument("-M0")

    def setupTerminateWhenMIDIFileDone(self):
        """
        When playing a MIDI file - we may choose to have our performance
        terminate when it reaches the end of the file.  We must, however,
        inform the system at setup that we want this termination to occur.
        """
        self.addCommandLineArgument("-T")
    
    def initialize(self):
        """ 
        initialize a performance (ie, parse the orc and sco)    
        """
        self.addCommandLineArgument("-k "+str(self._controlRate))
        self.addCommandLineArgument("-r "+str(self._sampleRate))
                
        message = OSC.OSCMessage()
        message.setAddress(self.address("/setup/initialize"))
        self.messageIO.sendMessage(message)

    def disconnect(self):
        """
        disconnect from the server
        """
        self.messageIO.stopListening()
        
    def shutdown(self):
        """ 
        initialize a performance (ie, parse the orc and sco)    
        """
        message = OSC.OSCMessage()
        message.setAddress(self.address("/shutdown"))
        self.messageIO.sendMessage(message)

        self.messageIO.stopListening()

    def shutdownServer(self):
        """ 
        shut the server down
        """
        message = OSC.OSCMessage()
        message.setAddress("/shutdown")
        self.messageIO.sendMessage(message)

        self.messageIO.stopListening()

    def playEventAsString(self, lineEventString):
        """ 
        instructs Csound to play a score event (using a string).
        Example
            csound.playEvent('i10 0 1')
            will play instr 10 start now (0) for a duration of 1            
        """        
        message = OSC.OSCMessage()
        message.setAddress(self.address("/event_string"))
        message.append(lineEventString)        
        self.messageIO.sendMessage(message)

    def playEvent(self, type, pfields):
        """ 
        instructs Csound to play a score event.
        Example
            csound.playEvent('i', [ 10, 0, 1 ])
            will play instr 10 start now (0) for a duration of 1
            
        type is either 'a', 'i', 'q', 'f', or 'e' 
        list of pfields (as floats)
        """
        
        message = OSC.OSCMessage()
        message.setAddress(self.address("/event"))
        message.append(type)
        
        for pField in pfields:
            message.append(float(pField))
            
        self.messageIO.sendMessage(message)

    def writeValueToChannel(self, channelName, value):
        """ 
        writes a value to a channel
        the chnget opcode can read the value sent using channelName as the key
        """         
        message = OSC.OSCMessage()
        message.setAddress(self.address("/channel/"+channelName))
        message.append(float(value))
        self.messageIO.sendMessage(message)
    


    def perform(self):
        """
        Perform from the beginning or where the performance left off
        """
        message = OSC.OSCMessage()
        message.setAddress(self.address("/perform"))
        self.messageIO.sendMessage(message)    

    def pause(self):
        """ 
        pause the main Csound performance
        """         
        message = OSC.OSCMessage()
        message.setAddress(self.address("/pause"))
        self.messageIO.sendMessage(message)

    def stop(self):
        """ 
        pause the main Csound performance
        """         
        message = OSC.OSCMessage()
        message.setAddress(self.address("/stop"))
        self.messageIO.sendMessage(message)

    def serverVersion(self):
        """
        get the server version info
        """
        return [self.serverMajorVersion, self.serverMinorVersion]

    def version(self):
        """
        get this API version 
        """
        return [self.majorVersion, self.minorVersion]
        
    def rewind(self):
        """ 
        rewind the main Csound score
        """         
        message = OSC.OSCMessage()
        message.setAddress(self.address("/rewind"))
        self.messageIO.sendMessage(message)

    def currentTime(self, timeout = 2, checkInterval = 0.05):
        """ 
        ask for the current time
        """
        # register before we send the message
        self.dispatcher.addToResponseWaitList("/current_time")

        # send the message
        message = OSC.OSCMessage()
        message.setAddress(self.address("/current_time"))
        self.messageIO.sendMessage(message)    
        
        try:
            result = self.waitForResponse("/current_time", timeout, checkInterval)
            return result.argument(0)

        except Exception, msg:
            print msg
            return -1

    def waitForPerformanceToFinish(self):
        """
        Checks to see if a regular score performance is done.  Waits forever.
        """
        self.dispatcher.addToResponseWaitList("/notification/done_performing")
        
        try:
            result = self.waitForResponse("/notification/done_performing", -1, 1)
            return result.argument(0)

        except Exception, msg:
            print msg
            return -1

    def waitForPerformanceToReachTime(self, time, timeout = 2, checkInterval = 0.1):
        """
        continues to ask for the current time until it reaches a certain value

        FUTURE WORK
        note: this could be optimized by having the server allow for time registrations
        and send a notification when it reaches the specified times
        """                
        try:
            done = 0
            while not done:
                result = self.currentTime(timeout, checkInterval)
                if result > time:
                    return 1
                
        except Exception, msg:
            print msg
            return -1





    #
    # MIDI
    #

    def sendMIDIStream(self, bytes):
        """
        Sends MIDI byte stream
        bytes = list of bytes to send
        
        Example: csound.sendMIDIStream( [1, 2, 3, 4] )
        """
        message = OSC.OSCMessage()
        message.setAddress(self.address("/midi_stream"))
        for b in bytes:
            message.append(b)
        self.messageIO.sendMessage(message)
    
    def sendMIDIEvent(self, b1, b2, b3):
        """
        Sends 3-byte MIDI message
        b1 = first byte
        b2 = 2nd byte
        b3 = 3rd byte
        """
        message = OSC.OSCMessage()
        message.setAddress(self.address("/midi_event"))
        message.append(b1)
        message.append(b2)
        message.append(b3)				
        self.messageIO.sendMessage(message)

    def playMIDINote(self, note, vel, duration, channel = 1):            
        """
        Sends a note on, waits for a specified duration and sends a note off
        note - note number
        vel - velocity
        duration - duration to wait
        channel - MIDI channel number
        """
        self.sendMIDIEvent(0x90 | (channel-1), note, vel)		
        time.sleep(duration)
        self.sendMIDIEvent(0x80 | (channel-1), note, 0)

    def sendMIDINoteAftertouch(self, note, amount, channel = 1):
        """
        Sends note aftertouch
        note - note numer
        amount - amount of aftertouch
        channel - MIDI channel
        """
        self.sendMIDIEvent(0xa0 | (channel-1), note, amount)

    def sendMIDIChannelAftertouch(self, note, amount, channel = 1):
        """
        Sends channel aftertouch
        note - note numer
        amount - amount of aftertouch
        channel - MIDI channel
        """
        self.sendMIDIEvent(0xd0 | (channel-1), note, amount)

    def playMIDINoteOn(self, note, vel, channel = 1):
        """
        sends note on
        note - note numer
        vel - velocity
        channel - MIDI channel
        """
        self.sendMIDIEvent(0x90 | (channel-1), note, vel)

    def playMIDINoteOff(self, note, channel = 1):
        """
        sends note off
        note - note numer
        channel - MIDI channel
        """
        self.sendMIDIEvent(0x80 | (channel-1), note, 0)
            
    def sendMIDIControl(self, control, value, channel = 1):
        """
        sends control
        control - control number
        value - the control value
        channel - MIDI channel
        """
        self.sendMIDIEvent(0xb0 | (channel-1), control, value)

    def sendMIDIProgramChange(self, program, channel = 1):
        """
        sends program change
        program - the program
        channel - MIDI channel
        """
        self.sendMIDIEvent(0xc0 | (channel-1), program, 0x00)

    def sendMIDIPitchBend(self, value, channel = 1):
        """
        sends pitch bend
        value - the pitch bend value (range is normalized -1 to 1)
        channel - MIDI channel
        """
        intValue = int( (value * 8191) + 8192 )        
        
        lsb = intValue & 0x7f
        msb = ((intValue << 1) & 0x7f00) >> 8
        
        # to put it back together
        #val = (msb << 7) + lsb
        
        self.sendMIDIEvent(0xe0 | (channel-1), lsb, msb)
