#!/usr/local/bin/python
#
#    Midi/In.py -- a higher-level interface to MIDI._In
#
#    Copyright (C) 1999-2000  Eric S. Tiedemann
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Library General Public
#    License as published by the Free Software Foundation; either
#    version 2 of the License, or (at your option) any later version.
#
#    This library 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
#    Library General Public License for more details.
#
#    You should have received a copy of the GNU Library General Public
#    License along with this library; if not, write to the Free
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#    Contact: Eric S. Tiedemann, est@hyperreal.org
#

_version = "0.9.0"

_types = __import__('types')
_midi = __import__('MIDI._In')

# set for C callback dispatch..otherwise dispatch is done in python
_Ccb = 1

class MidiIn:
    """MIDI input stream parser."""
    
    # range of values various MIDI data can assume
    __token_ranges = (
        ((1,16), 'channel'),
        ((0,16), 'nchannels'),
        ((0,1), 'bool'),
        ((0,119), 'control'),
        ((0, 2**14 - 1), 'bend', 'position'), # 14-bit values
        ((0, 127),
         'key', 'velocity', 'value', 'program', 'pressure', 'mtcval', 'song'),
        )
    
    # create a map from token names to value ranges
    __tokens = {}
    
    for __tup in __token_ranges:
        __r = __tup[0]
        __ts = __tup[1:]
        for __t in __ts:
            __tokens[__t] = __r

    # grammar of MIDI messages received from MIDI._In
    __grammar = {
        # channel voice messages
        'note-off':('channel', 'key', 'velocity',),
        'note-on':('channel', 'key', 'velocity',),
        'poly-key-pressure':('channel', 'key', 'velocity',),
        'control-change':('channel', 'control', 'value',),
        'program-change':('channel', 'program',),
        'channel-pressure':('channel', 'pressure',),
        # the two bend bytes are combined in one 14-bit value
        'pitch-bend':('channel', 'bend',),
        
        # channel mode messages
        'all-sound-off':('channel',),
        'reset-all-controllers':('channel',),
        # bool is 0 or 1
        'local-control':('channel', 'bool',),
        'all-notes-off':('channel',),
        'omni-off':('channel',),
        'omni-on':('channel',),
        # nchannels is number of channels or 0 for number of voices in receiver
        'mono-on':('channel', 'nchannels',),
        'poly-on':('channel',),
        
        # system common messages
        'MTC-quarter-frame':('mtcval',),
        # the two position bytes are combined in one 14-bit value
        'song-position-pointer':('position',),
        'song-select':('song',),
        'tune-request':(),
        
        # system real-time messages
        'timing-clock':(),
        'start':(),
        'continue':(),
        'stop':(),
        'active-sensing':(),
        'system-reset':(),
        }

    # the MIDI callback structure is arranged in a table with a
    # top-level dictionary and nested lists.  all this is constructed
    # lazily as various slots are assigned.  the callback dispatcher
    # (self.cb) knows how to traverse this lazy structure.  the
    # structure is modified via calls to __setitem__ below.
    
    def __setitem__(self, i, v):
        #print "%s.__setitem__(%s, %s)" % (self, i, v)

        if v != None and not callable(v):
            raise TypeError, "value is not callable or None"

        # normalize indices to tuple
        if type(i) != _types.TupleType: i = (i,)

        if i[0] == Ellipsis:
            # an initial ellipsis can be used to register one function
            # for *all* message types
            if len(i) != 1:
                raise ValueError, "too many terms in subscript"
            else:
                self.__tab = v
        else:
            msg = i[0]
            if not type(msg) == _types.StringType:
                raise TypeError, "the first arg must be a string"
            if not self.__grammar.has_key(msg):
                raise ValueError, "%s is not a valid message name" % msg

            if not self.__tab or type(self.__tab) != _types.DictType:
                # ok..we need to construct that top-level dict
                # handing __zap() the current scalar value for __tab.
                self.__tab = {msg:self.__zap(msg, self.__tab,
                                             i[1:], v, self.__grammar[msg])}
            else:
                # top-level dict already exists, so __zap() should be
                # called using whatever's already in the MSG slot
                self.__tab[msg] = self.__zap(msg, self.__tab.get(msg),
                                             i[1:], v, self.__grammar[msg])

        if _Ccb:
            self.__m.settab(self.__tab)

    # the scarier function for modifying the callback table :)
    #
    # it's a recursive function for chewing down remaining setitem
    # indices (IDXS) and grammar terms (GRAM) that returns a new value
    # for the table based on the current one for this level passed in
    # (TAB) and the callback value V.  it's recursive nature means the
    # if their are any exceptions, no modifications are done.  MSG is
    # the message type and is used for error messages.
    #
    # each index can be a number, a simple numeric slice, or an ellipsis
    def __zap(self, msg, tab, idxs, v, gram):
        #print 'zap', msg, tab, idxs, v, gram
        
        if not gram and idxs:
            raise ValueError, "too many indices for %s" % msg
        elif not idxs:
            return v

        i = idxs[0]
        tok = gram[0]
        r = self.__tokens[tok]
        
        if i == Ellipsis:
            if type(tab) != _types.ListType:
                tab = [tab] * (r[1] + 1)

            for i in range(len(tab)):
                tab[i] = self.__zap(msg, tab[i], idxs[1:], v, gram[1:])

            return tab

        elif type(i) == _types.IntType:
            #print i, r
            if i < r[0] or i > r[1]:
                raise ValueError, "index out of range for %s" % gram[0]

            if type(tab) != _types.ListType:
                tab = [tab] * (r[1] + 1)
            #print tab
            tab[i] = self.__zap(msg, tab[i], idxs[1:], v, gram[1:])
            #print tab

            return tab

        elif type(i) == _types.SliceType:
            if i.step:
                raise TypeError, "stepping ranges aren't allowed as indices"

            start, stop = i.start, i.stop

            if not start: start = r[0]
            if not stop: stop = r[1]

            if type(start) != _types.IntType or type(stop) != _types.IntType:
                raise TypeError, "ranges must be of integers"
            
            if start < r[0] or stop > r[1]:
                raise ValueError, "invalid range for %s" % gram[0]

            if type(tab) != _types.ListType:
                tab = [tab] * (r[1] + 1)

            for i in range(start, stop+1):
                tab[i] = self.__zap(msg, tab[i], idxs[1:], v, gram[1:])

            return tab

        else:
            raise TypeError, "invalid index %s" % i

    # the callback dispatcher.  MSG and ARGS come direct from MIDI._In.
    # this isn't used if _Ccb is set.
    def __cb(self, msg, *args):
        #print msg, args
        
        if not self.__tab:
            return

        if type(self.__tab) != _types.DictType:
            apply(self.__tab, (msg,) + args)
            return
         
        cb = self.__tab.get(msg)

        args0 = args
        
        # tear down the callback table structure based on number of
        # args received.
        while cb and type(cb) == _types.ListType:
            if not args:
                raise RuntimeError, \
                      "insufficient arguments for message type %s" % msg
            cb = cb[args[0]]
            args = args[1:]

        if cb:
            apply(cb, (msg,) + args0)

    def __init__(self, sysex_cb = None):
        self.__tab = None

        if _Ccb:
            if sysex_cb:
                self.__m = _midi._In.new1(self.__tab, sysex_cb)
            else:
                self.__m = _midi._In.new1(self.__cb)
        else:
            if sysex_cb:
                self.__m = _midi._In.MidiIn(self.__cb, sysex_cb)
            else:
                self.__m = _midi._In.MidiIn(self.__cb)

    def inbytes(self, bs):
        self.__m.inbytes(bs)

    def read(self, f):
        self.__m.read(f)


if (__name__ == '__main__'):

    def testloop():
        import os
        
        def scb(s): print 'system exclusive:', map(ord, s)
        
        m = MidiIn(scb)
        
        def foo(msg, *args): print msg, args
        
        m[...] = foo

        while 1:
            bs = os.read(0, 1000)
            m.inbytes(bs)
            
    import sys

    if len(sys.argv) > 1:
        import profile
        p = profile.run('testloop()', 'midistats')
        import pstats
        p = pstats.Stats('midistats')
        p.sort_stats('cumulative').print_stats(20)
        p.sort_stats('time').print_stats(20)
    else:
        testloop()
