# -*- coding: ISO-8859-1 -*-

# std library
from struct import unpack

# uhh I don't really like this, but there are so many constants to 
# import otherwise
from constants import *

from EventDispatcher import EventDispatcher

class MidiFileParser:

    """
    
    The MidiFileParser is the lowest level parser that see the data as 
    midi data. It generates events that gets triggered on the outstream.
    
    """

    def __init__(self, raw_in, outstream):

        """
        raw_data is the raw content of a midi file as a string.
        """

        # internal values, don't mess with 'em directly
        self.raw_in = raw_in
        self.dispatch = EventDispatcher(outstream)

        # Used to keep track of stuff
        self._running_status = None




    def parseMThdChunk(self):
        
        "Parses the header chunk"
        
        raw_in = self.raw_in

        header_chunk_type = raw_in.nextSlice(4)
        header_chunk_zise = raw_in.readBew(4)

        # check if it is a proper midi file
        if header_chunk_type != 'MThd':
            raise TypeError, "It is not a valid midi file!"

        # Header values are at fixed locations, so no reason to be clever
        self.format = raw_in.readBew(2)
        self.nTracks = raw_in.readBew(2)
        self.division = raw_in.readBew(2)
        
        # Theoretically a header larger than 6 bytes can exist
        # but no one has seen one in the wild
        # But correctly ignore unknown data if it is though
        if header_chunk_zise > 6:
            raw_in.moveCursor(header_chunk_zise-6)

        # call the header event handler on the stream
        self.dispatch.header(self.format, self.nTracks, self.division)



    def parseMTrkChunk(self):
        
        "Parses a track chunk. This is the most important part of the parser."
        
        # set time to 0 at start of a track
        self.dispatch.reset_time()
        
        dispatch = self.dispatch
        raw_in = self.raw_in
        
        # Trigger event at the start of a track
        dispatch.start_of_track(self._current_track)
        # position cursor after track header
        raw_in.moveCursor(4)
        # unsigned long is 4 bytes
        tracklength = raw_in.readBew(4)
        track_endposition = raw_in.getCursor() + tracklength # absolute position!

        while raw_in.getCursor() < track_endposition:
        
            # find relative time of the event
            time = raw_in.readVarLen()
            dispatch.update_time(time)

            # be aware of running status!!!!
            peak_ahead = raw_in.readBew(move_cursor=0)
            if (peak_ahead & 0x80):
                # the status byte has the high bit set, so it
                # was not running data but proper status byte
                status = raw_in.readBew()
                if not peak_ahead in [META_EVENT, SYSTEM_EXCLUSIVE]:
                    self._running_status = status
            else:
                # use that darn running status
                status = self._running_status
                # could it be illegal data ?? Do we need to test for that?
                # I need more example midi files to be shure.
                
                # Also, while I am almost certain that no realtime 
                # messages will pop up in a midi file, I might need to 
                # change my mind later.

            # we need to look at nibbles here
            hi_nible, lo_nible = status & 0xF0, status & 0x0F

            # match up with events

            # Is it a meta_event ??
            # these only exists in midi files, not in transmitted midi data
            # In transmitted data META_EVENT (0xFF) is a system reset
            if status == META_EVENT:
                meta_type = raw_in.readBew()
                meta_length = raw_in.readVarLen()
                meta_data = raw_in.nextSlice(meta_length)
                if not meta_length: return
                dispatch.meta_event(meta_type, meta_data)
                if meta_type == END_OF_TRACK: return


            # Is it a sysex_event ??
            elif status == SYSTEM_EXCLUSIVE:
                # ignore sysex events
                sysex_length = raw_in.readVarLen()
                # don't read sysex terminator
                sysex_data = raw_in.nextSlice(sysex_length-1)
                # only read last data byte if it is a sysex terminator
                # It should allways be there, but better safe than sorry
                if raw_in.readBew(move_cursor=0) == END_OFF_EXCLUSIVE:
                    eo_sysex = raw_in.readBew()
                dispatch.sysex_event(sysex_data)
                # the sysex code has not been properly tested, and might be fishy!


            # is it a system common event?
            elif hi_nible == 0xF0: # Hi bits are set then
                data_sizes = {
                    MTC:1,
                    SONG_POSITION_POINTER:2,
                    SONG_SELECT:1,
                }
                data_size = data_sizes.get(hi_nible, 0)
                common_data = raw_in.nextSlice(data_size)
                common_type = lo_nible
                dispatch.system_common(common_type, common_data)
            

            # Oh! Then it must be a midi event (channel voice message)
            else:
                data_sizes = {
                    PATCH_CHANGE:1,
                    CHANNEL_PRESSURE:1,
                    NOTE_OFF:2,
                    NOTE_ON:2,
                    AFTERTOUCH:2,
                    CONTINUOUS_CONTROLLER:2,
                    PITCH_BEND:2,
                }
                data_size = data_sizes.get(hi_nible, 0)
                channel_data = raw_in.nextSlice(data_size)
                event_type, channel = hi_nible, lo_nible
                dispatch.channel_messages(event_type, channel, channel_data)


    def parseMTrkChunks(self):
        "Parses all track chunks."
        for t in range(self.nTracks):
            self._current_track = t
            self.parseMTrkChunk() # this is where it's at!
        self.dispatch.eof()



if __name__ == '__main__':

    import sys

    # get data
    test_file = 'test/midifiles/minimal.mid'
    test_file = 'test/midifiles/cubase-minimal.mid'
    test_file = 'test/midifiles/Lola.mid'
    test_file = sys.argv[1]
#    f = open(test_file, 'rb')
#    raw_data = f.read()
#    f.close()
#    
#    
#    # do parsing
    from MidiToText import MidiToText
    from RawInstreamFile import RawInstreamFile

    midi_in = MidiFileParser(RawInstreamFile(test_file), MidiToText())
    midi_in.parseMThdChunk()
    midi_in.parseMTrkChunks()
    
