#!/usr/bin/python
# -*- coding: utf-8 -*-

from BeautifulSoup import BeautifulStoneSoup
from collections import defaultdict
from os.path import splitext, basename
import re, sys

if len(sys.argv) < 2:
    print "Usage: " + sys.argv[0] + " <musicxml> <header filename (opt)>"
    exit()
fname = sys.argv[1]
fheader = None
if len(sys.argv) == 3:
    fheader = sys.argv[2]

freq = {
        "1" : 1000000,
        "2" : 8000000,
        "3" : 16000000,
        }

freq_map = {
    # Name  : Freq. (Hz) 
    u"C8 Eighth octave" : 4186.01,
    u"B7" : 3951.07,
    u"A♯7/B♭7" : 3729.31,
    u"A7" : 3520,
    u"G♯7/A♭7" : 3322.44,
    u"G7" : 3135.96,
    u"F♯7/G♭7" : 2959.96,
    u"F7" : 2793.83,
    u"E7" : 2637.02,
    u"D♯7/E♭7" : 2489.02,
    u"D7" : 2349.32,
    u"C♯7/D♭7" : 2217.46,
    u"C7 Double high C" : 2093,
    u"B6" : 1975.53,
    u"A♯6/B♭6" : 1864.66,
    u"A6" : 1760,
    u"G♯6/A♭6" : 1661.22,
    u"G6" : 1567.98,
    u"F♯6/G♭6" : 1479.98,
    u"F6" : 1396.91,
    u"E6" : 1318.51,
    u"D♯6/E♭6" : 1244.51,
    u"D6" : 1174.66,
    u"C♯6/D♭6" : 1108.73,
    u"C6 Soprano C (High C)" : 1046.5,
    u"B5" : 987.767,
    u"A♯5/B♭5" : 932.328,
    u"A5" : 880,
    u"G♯5/A♭5" : 830.609,
    u"G5" : 783.991,
    u"F♯5/G♭5" : 739.989,
    u"F5/E♯5" : 698.456,
    u"E5/F♭5" : 659.255,
    u"D♯5/E♭5" : 622.254,
    u"D5" : 587.33,
    u"C♯5/D♭5" : 554.365,
    u"C5 Tenor C" : 523.251,
    u"C♭5" : 493.883,
    u"B4" : 493.883,
    u"A♯4/B♭4" : 466.164,
    u"A4 A440" : 440,
    u"G♯4/A♭4" : 415.305,
    u"G4" : 391.995,
    u"F♯4/G♭4" : 369.994,
    u"F4" : 349.228,
    u"E4/F♭4" : 329.628,
    u"D♯4/E♭4" : 311.127,
    u"D4" : 293.665,
    u"C♯4/D♭4" : 277.183,
    u"C4 Middle C" : 261.626,
    u"C♭4" : 246.942,
    u"B♯3" : 261.626,
    u"B3" : 246.942,
    u"A♯3/B♭3" : 233.082,
    u"A3" : 220,
    u"G♯3/A♭3" : 207.652,
    u"G3" : 195.998,
    u"F♯3/G♭3" : 184.997,
    u"F3" : 174.614,
    u"E3" : 164.814,
    u"D♯3/E♭3" : 155.563,
    u"D3" : 146.832,
    u"C♯3/D♭3" : 138.591,
    u"C3 Low C" : 130.813,
    u"B♯2" : 130.813,
    u"B2/C♭3" : 123.471,
    u"A♯2/B♭2" : 116.541,
    u"A2" : 110,
    u"G♯2/A♭2" : 103.826,
    u"G2" : 97.9989,
    u"F♯2/G♭2" : 92.4986,
    u"F2" : 87.3071,
    u"E♯2" : 87.3071,
    u"E2" : 82.4069,
    u"D♯2/E♭2" : 77.7817,
    u"D2" : 73.4162,
    u"C♯2/D♭2" : 69.2957,
    u"C2 Deep C" : 65.4064,
    u"B1" : 61.7354,
    u"A♯1/B♭1" : 58.2705,
    u"A1" : 55,
    u"G♯1/A♭1" : 51.9131,
    u"G1" : 48.9994,
    u"F♯1/G♭1" : 46.2493,
    u"F1" : 43.6535,
    u"E1" : 41.2034,
    u"D♯1/E♭1" : 38.8909,
    u"D1" : 36.7081,
    u"C♯1/D♭1" : 34.6478,
    u"C1 Pedal C" : 32.7032,
    u"B0" : 30.8677,
    u"A♯0/B♭0" : 29.1352,
    u"A0 Double Pedal A" : 27.5,
}

def raw_input_def(prompt, default):
    while True:
        data = raw_input(prompt + ' [' + default + ']: ')
        if not (data == '' and default == ''):
            break

    if data == '':
        return default
    else:
        return data

def process_part(p, fq):

    data = {}
    prescale = {
        "9" : 1024,
        "8" : 512,
        "7" : 256,
        "6" : 128,
        "5" : 64,
        "4" : 32,
        "3" : 16,
        "2" : 8,
        "1" : 1,
        }

    data=[]
    data_starts=[]
    num_voices = 0  

    for measure in soup('part')[p].findAll('measure'):
        mdata = defaultdict(list)
        mdata_starts = defaultdict(list)
        # keeps track of the previous note so we can
        # tell if two notes are tied together and
        # mark them contiguous
        prev_note = None

        for note in measure.findAll('note'):
            if note.chord:
                next  # skip chords

            # this is a poor-man's musicxml parser :(
            # some validation checks before we parse
            # every note has one and only one 
            # duration and voice
            if not (len(note.duration.contents) == 1 and \
                    len(note.voice.contents) == 1):
                raise ValueError
            v = note.voice.contents[0] 
            d = note.duration.contents[0]
            if (note.rest):
                # fill the note data with 0s
                mdata[v].extend([0 for x in range(0,int(d))])
                # mark the beginning of the rest 
                mdata_starts[v].append(1)
                mdata_starts[v].extend([ 0 for x in range(0,int(d)-1)]) 
            else:
                # every non-rest must have one and only one
                # step and octave
                if not (len(note.step.contents) == 1 and \
                        len(note.octave.contents) ==1):
                    raise ValueError
                # start of a new note
                notestr = unicode(note.step.contents[0])
                if note.alter:
                    if note.alter.contents[0] == "-1":
                        notestr += u"♭"
                    elif note.alter.contents[0] == "1":
                        notestr += u"♯"
                    else:
                        raise ValueError
                notestr += note.octave.contents[0]
                # lookup a corresponding frequency
                # for the note in the mapping dict
                lookup = False 
                for i in freq_map.keys():
                    if re.search(notestr,i):
                        lookup = freq_map[i]
                        break
                if not lookup:
                    sys.exit("Unable to find match for " + notestr)

                prev_note = lookup
                mdata[v].extend([ lookup for x in range(0,int(d))])

                # mark the beginning of the rest or note
                # and fill the rest of the start data
                # with 0s (unless it's tied)
                if (prev_note == lookup and \
                        note.find('tie', attrs={'type':'stop'})):
                    mdata_starts[v].extend([ 0 for x in range(0,int(d))])
                else:
                    mdata_starts[v].append(1)
                    mdata_starts[v].extend([ 0 for x in range(0,int(d)-1)]) 


        # update the number of voices
        if num_voices < len(mdata.keys()):
            num_voices = len(mdata.keys())

        # add the measure data to the frequency array
        # the first voice will take precedence if the 
        # different voices are merged. 
        merged = []
        merged_starts = []
        for voice in sorted(mdata.keys()):
            for index in range(0, len(mdata[voice])):
                if len(merged) == index:
                    merged.append( mdata[voice][index] )
                    merged_starts.append( mdata_starts[voice][index] )
                elif len(merged) > index:
                    # only overwrite the merged data if the 
                    # value at the index is 0
                    if not merged[index]:
                        merged[index] = mdata[voice][index]
                        merged_starts[index] = mdata_starts[voice][index]
                else:
                    raise ValueError
        data.extend(merged)
        data_starts.extend(merged_starts)


    # filter prescales removing ones that give values > 255
    for k in sorted(prescale.keys()):
        ps = prescale[k]
        tmp = [int(round((fq - (2*ps*i))/(2*ps*i),0)) if i else 0 for i in data]
        if max(tmp) > 255:
            prescale.pop(k)

    if not len(prescale.keys()):
        sys.exit("ERROR: all prescales invalid")

    for i in sorted(prescale.keys()):
        print "  %2s) %6s" % (i, str(prescale[i]))

    print "!! IMPORTANT !! Prescaler values of 2,4,16,32,128,512 are ONLY valid for the attiny85 TIMER1"
    ps = prescale[ raw_input_def("Pick prescale", str(min(prescale.keys())))]
    t_data = [int(round((fq - (2*ps*i))/(2*ps*i),0)) if i else 0 for i in data]

    notes = []
    delays = []
    for pos in range(0, len(t_data)):
        if data_starts[pos]:
            notes.append(t_data[pos])
            delays.append(1)
        else:
            delays[-1] += 1

    rdata = {}
    rdata['prescale'] = ps
    rdata['notes'] = notes
    rdata['delays'] = delays
    rdata['freq'] = fq
    rdata['voices'] = num_voices
    return rdata


f = open(fname)
data = f.read()
f.close()

soup = BeautifulStoneSoup(data)
data = []
num_parts = len(soup('part'))

if num_parts < 1:
    print >> sys.stderr, "ERROR: no parts found in file " + fname
    exit()

for i in sorted(freq.keys()):
    print "%2s) %10s" % (i, str(freq[i]))
fq = freq[raw_input_def("Pick Frequency (Hz)", "3")]


part_select = 0
d = []
print "Found " + str(num_parts) + " parts in " + fname

while part_select < num_parts:
    p = raw_input_def("Choose a part to process ('q' to quit) [0-" + str(num_parts-1) + "]", str(part_select))
    if p == 'q':
        break
    else:
        d.append(process_part(int(p),fq))
    part_select += 1

# create header file
name = basename(splitext(fname)[0])
name = re.sub(re.compile("[-]"),"_",name)

hdefines = "#ifndef " + name.upper() + "\n" + \
        "#define " + name.upper() + "\n" + \
        "#include <stdint.h>\n" + \
        "#include <avr/pgmspace.h>\n\n"

hcomments = ""
harrays = ""

for cnt in range(0,len(d)):
    p = d[cnt]
    scnt = str(cnt)
    hdefines += "#define "+ name.upper() + scnt+" "+str(p['prescale'])+","+\
            name + scnt+ "_notes,"+ name + scnt+\
            "_delays,sizeof("+ name + scnt+"_notes)/sizeof(*("+ name + \
            scnt + "_notes)),sizeof("+ name + scnt+\
            "_delays)/sizeof(*("+ name + scnt+ \
            "_delays))\n"

    hcomments += "\n    Part: " + scnt + \
             "\n    Freq: " + str(p['freq']) + \
             "\n    Number of voices: " + str(p['voices']) + \
             "\n    Note length: " + str(len(p['notes'])) + \
             "\n    Delay length: " + str(len(p['delays']))  + \
             "\n"


    harrays += "const uint8_t "+ name + scnt+"_notes[] PROGMEM = {\n" + \
            ",".join( [str(x) for x in p['notes']]) + \
            "\n};\n"
    harrays += "const uint8_t "+ name + scnt+"_delays[] PROGMEM = {\n" + \
            ",".join( [str(x) for x in p['delays']]) + \
            "\n};\n\n"



if (fheader):
    f = open(fheader,'w')
    f.write( hdefines + \
     "/*\n" + hcomments + "\n*/\n" + \
     harrays + "#endif\n")
    f.close
else:
    print hdefines
    print "/*\n" + hcomments + "\n*/\n"
    print harrays + "#endif\n"


