
import sys, re, copy

import fraction

class Error(Exception): pass

# TODO: More time signatures

# TODO: breaking odd lengths (and |x x-x x| not |x x2 x|)

notes = {
  'c':0, 'd':2, 'e':4, 'f':5, 'g':7, 'a':9, 'b':11
}
notes_inverse = dict([ (b,a) for a,b in notes.items() ])

blank_state = {
  'c':0, 'd':0, 'e':0, 'f':0, 'g':0, 'a':0, 'b':0
}

major_keys = {
   "c"  : "",
   "f"  : "_b",
   "bb" : "_b_e",
   "eb" : "_b_e_a",
   "ab" : "_b_e_a_d",
   "db" : "_b_e_a_d_g",
   "gb" : "_b_e_a_d_g_c",
   "cb" : "_b_e_a_d_g_c_f",
   "g"  : "^f",
   "d"  : "^f^c",
   "a"  : "^f^c^g",
   "e"  : "^f^c^g^d",
   "b"  : "^f^c^g^d^a",
   "f#" : "^f^c^g^d^a^e",
   "c#" : "^f^c^g^d^a^e^b"
}

minor_keys = {
   "a"  : major_keys["c"],
   "d"  : major_keys["f"],
   "g"  : major_keys["bb"],
   "c"  : major_keys["eb"],
   "f"  : major_keys["ab"],
   "bb" : major_keys["db"],
   "eb" : major_keys["gb"],
   "ab" : major_keys["cb"],
   "e"  : major_keys["g"],
   "b"  : major_keys["d"],
   "f#" : major_keys["a"],
   "c#" : major_keys["e"],
   "g#" : major_keys["b"],
   "d#" : major_keys["f#"],
   "a#"  : major_keys["c#"]
}

note_pattern = r"\s*(\^|_|=|)([za-gA-G])([',]*)([0-9]*)(/)?([0-9]*)(-)?(\s*)"
bar_pattern = r"[|:[\]/]+\s*"
inline_part_pattern = r"\[P:\s*([^\]]*)\]\s*"
inline_m2h_pattern = r"\[h:\s*([^\]]*)\]\s*"
inline_pattern = r"\[[^\]]*\]\s*"
annotation_pattern = r'\s*("[^"]*"|![^!]*!)\s*'

def int_from_string(string):
    match = re.match("[0-9]+$", string)
    if not match:
        raise Error("Bad number: " + string)
        
    return int(string)

def fraction_from_string(string):
    """ Note: returns tuple, not fraction. """
    try:
        return (int_from_string(string), 1)
    except Error:
        match = re.match("([0-9]+)/([0-9]+)$", string)
        if not match:
            raise Error("Bad fraction: " + string)
        
        return (int(match.group(1)),int(match.group(2)))

def good_length(length):
    n = length.num
    while n%2 == 0: n //= 2
    return n in (1,3)

def read_key(text):
    original = text
    text = text.lower().strip()

    set = major_keys
        
    if text[-3:] == "maj": text = text[:-3]
    elif text[-1:] == "m":
        text = text[:-1]
        set = minor_keys
    elif text[-3:] == "min":
        text = text[:-3]
        set = minor_keys
        
    if text not in set:
        raise Error("Can't understand key signature: " + original)
        
    state = blank_state.copy()
    read(set[text], state)
    return state


def read_note(groups, state=None, unit=1):
    """ Note: alters state """
    
    if not state: state = blank_state.copy()
    
    accent, letter, octaver, numerator, slash, denominator, slur, whitespace = groups

    if letter == "z":
        note = None
    else:    
        octave = 0
        if letter.isupper():
            octave -= 1
            letter = letter.lower()
        
        for item in octaver:
            if item == "'": octave += 1
            if item == ",": octave -= 1
        
        if accent == "^": state[letter] = 1
        elif accent == "_": state[letter] = -1
        elif accent == "=": state[letter] = 0
    
        note = notes[letter] + state[letter] + octave*12
    
    if slash and not denominator: denominator = "2"
    elif not denominator: denominator = "1"
    if not numerator: numerator = "1"
    
    return note, fraction.fraction(int_from_string(numerator), int_from_string(denominator)) * unit, (slur != None), whitespace

def read(text, state=None, key_state=None, unit=1):
    if not key_state: key_state = blank_state.copy()
    if not state: state = key_state.copy()

    result = [ ]
    
    text = text.strip()
    while text:
        if text[:1] == "|":
            for key, value in key_state.items():
                state[key] = value
            text = text[1:]
        else:
            match = re.match(note_pattern, text)
            result.append( read_note(match.groups(), state, unit) )
            if not match:
                raise Error("Can't understand notation: " + text.split()[0])
            
            text = text[match.end():]
        text = text.lstrip()
        
    return result



class Struct:
    def __init__(self, **kwargs):
        for key in kwargs:
            setattr(self, key, kwargs[key])
            
    def but_with(self, **kwargs):
        result = copy.copy(self)
        for key in kwargs:
            setattr(result, key, kwargs[key])
        return result
        

class Notation(Struct):
    def to_abc(self, state, key_state, unit): 
        return self.text

class Annotation(Notation): pass

class Merge_blocker(Struct):
    def to_abc(self, state, key_state, unit): return ""

class Bar(Struct):    
    def to_abc(self, state, key_state, unit): 
        for key in key_state:
            state[key] = key_state[key]
        return self.text

class Note(Struct): 
    def to_abc(self, state, key_state, unit):
        def split(a, b):
            return self.but_with(length=a,slur=True).to_abc(state,key_state,unit) + \
                   self.but_with(length=b,bar_position=self.bar_position+a).to_abc(state,key_state,unit)
    
        if not good_length(self.length):
            n = 1
            while n*2 < self.length.num: n *= 2
            a = fraction.fraction(n, self.length.den)
            b = self.length-a
            
            mid_ab = self.bar_position + a
            mid_ba = self.bar_position + b
            
            if mid_ab.multiple_of(self.emphasis): return split(a,b)
            elif mid_ba.multiple_of(self.emphasis): return split(b,a)
            elif mid_ab.multiple_of(self.beat): return split(a,b)
            elif mid_ba.multiple_of(self.beat): return split(b,a)
            elif mid_ab.multiple_of(self.shatter): return split(a,b)
            elif mid_ba.multiple_of(self.shatter): return split(b,a)
            else: return split(a,b)
            
#            return self.but_with(length=a,slur=True).to_abc(state,key_state,unit) + \
#                   self.but_with(length=b,bar_position=self.bar_postion+a).to_abc(state,key_state,unit)
        
        if self.value == None:
            note = "z"
        else:
            octave = self.value // 12
            note = self.value % 12
    
            if note in notes_inverse:
                note = write_set_state(notes_inverse[note],0,state) + notes_inverse[note]
            else:
                note = write_set_state(notes_inverse[note+1],-1,state) + notes_inverse[note+1]
        
            if octave < 0:
                note = note.upper() + "," * (-octave-1)
            else:
                note = note + "'" * octave
                
        return note + self.length.abc_repr() + (self.slur and "-" or "") + self.whitespace
        
    
class Song:
    def __init__(self, lines):
        self.header = [ ]
        self.index = None
        self.title = ""
        self.key_state = None
#        self.notes = [ ]
        self.music = [ ]
#        self.bar = None
        self.unit = None
        self._shatter = fraction.fraction(1,2)
        self._beat = fraction.fraction(1)
        self._emphasis = fraction.fraction(2)

        self.parts = [ ]
        self._part = None
                
        self._final = None
        self._horizontality = 1.0
        self._state = None
        self._bar_position = fraction.fraction(0)
        
        self._hornpipe_factor = 1
        
        in_header = True
        
        for line in lines:
            omit = False
            if line[:1] == '%':
                if line[:5].upper() == "%%M2H":
                    omit = True
                    self._handle_command(line[5:])
            elif line[:1].isalpha() and line[1:2] == ':':
                letter = line[0].upper()
                body = line[2:].strip()
                
                if letter == "X":
                    self.index = body
                if letter == "T":
                    self.title = body
                if letter == "K":
                    self.key_state = read_key(line[2:])
                    self._state = self.key_state.copy()
                elif letter == "L":
                    self.unit = fraction.fraction(*fraction_from_string(body)) * 4
                #elif letter == "M":
                #    top, bottom = fraction_from_string(body)
                #    if bottom != 4:
                #        raise "Can't do this time signature."
                #    self.bar = top
                elif letter == "P" and self.key_state:
                    self._part = body
                    self.parts.append(body)
                    omit = True
            else:
                if self.key_state is None:
                    raise Error("Missing K:")
#                if self.bar is None:
#                    raise "Missing M:"
                if self.unit is None:
                    raise Error("Missing L:")
                    
                #self.notes.extend(read(line, state, self.key_state, self.unit))
                self._read_line(line)
                self.music.append(Notation(text="\n", part=self._part))
                omit = True
                
            if not omit:
                if in_header:
                    self.header.append(line)
                else:
                    self.music.append(Notation(text=line+"\n",part=self._part))
                    
            if self.key_state: in_header = False
                
        # Default final if not specified
        actual_final = None
        for item in self.music:
            if isinstance(item, Note) and item.value != None:
                actual_final = item.value
        for item in self.music:
            if isinstance(item, Note) and item.final is None:
                item.final = actual_final

    def _handle_command(self, text):                
        items = text.split()
        omit = True
        if len(items) == 2 and items[0].lower() == "final":
            match = re.match(note_pattern, items[1])
            if not match:
                raise Error("Bad note in %%M2H final")
            self._final = read_note(match.groups(), self.key_state.copy())[0]
        elif len(items) == 2 and items[0].lower() == "emphasis":
            self._emphasis = fraction.fraction(*fraction_from_string(items[1]))
        elif len(items) == 2 and items[0].lower() == "beat":
            self._beat = fraction.fraction(*fraction_from_string(items[1]))
        elif len(items) == 2 and items[0].lower() == "tick":
            self._shatter = fraction.fraction(*fraction_from_string(items[1]))
        elif len(items) == 2 and items[0].lower() == "horizontality":
            try:
                self._horizontality = float(items[1])
            except ValueError:
                raise Error("Bad number: " + items[1])
        elif len(items) == 1 and items[0].lower() == "break":
            self.music.append(Merge_blocker(part=self._part))
        else:
            raise Error("Can't understand declaration: "+text)

    def _read_line(self, text):
        last_note = "neither None nor an integer"
        text = text.rstrip()
        while text:
            text = text.lstrip()
            
            if text[:1] == ">":
                assert self.music and isinstance(self.music[-1], Note), Error("Spurious >")
                self.music[-1].length = self.music[-1].length * fraction.fraction(3,2)
                self._hornpipe_factor = fraction.fraction(1,2)
                text = text[1:]
                continue
            if text[:1] == "<":
                assert self.music and isinstance(self.music[-1], Note), Error("Spurious >")
                self.music[-1].length = self.music[-1].length * fraction.fraction(1,2)
                self._hornpipe_factor = fraction.fraction(3,2)
                text = text[1:]
                continue
                
                
            match = re.match(inline_part_pattern, text)
            if match:
                self._part = match.group(1).strip()
                self.parts.append(self._part)
                text = text[match.end():]
                continue
            
            match = re.match(inline_m2h_pattern, text)
            if match:
                self._handle_command(match.group(1).strip())
                text = text[match.end():]
                continue
                
            match = re.match(inline_pattern, text)
            if match:
                self.music.append(Notation(text=match.group(0), part=self._part))
                text = text[match.end():]
                continue
            
            match = re.match(bar_pattern, text)
            if match:
                self.music.append(Bar(text=match.group(0), part=self._part))
            
                for key, value in self.key_state.items():
                    self._state[key] = value
                self._bar_position = fraction.fraction(0)
                
                text = text[match.end():]
                continue
            
            match = re.match(annotation_pattern, text)
            if match:
                self.music.append(Annotation(text=match.group(0), part=self._part))
                text = text[match.end():]
                continue
                
            match = re.match(note_pattern, text)
            if match:
                value, length, slur, whitespace = read_note(match.groups(), self._state, self.unit)
                
                #if last_note == value:
                #    # Prevent merging, so all parts emphasize this repeat
                #    self.music.append(Merge_blocker(part=self._part))
                
                self.music.append(Note(value=value,
                                       length=length * self._hornpipe_factor,
                                       slur=slur,
                                       whitespace=whitespace,
                                       bar_position=self._bar_position,
                                       emphasis=self._emphasis,
                                       beat=self._beat,
                                       shatter=self._shatter,
                                       final=self._final,
                                       horizontality=self._horizontality,
                                       part=self._part))
                self._bar_position = self._bar_position + length
                self._hornpipe_factor = 1
                last_note = value
            
                text = text[match.end():]
                continue
                
            raise Error("Can't understand notation: " + text.split()[0])


    def music_to_abc(self, music):
        result = [ ]
        state = self.key_state.copy()
        for event in music:
            result.append(event.to_abc(state, self.key_state, self.unit))
        
        return "".join(result)
        
    def transpose(self, amount): #TODO: key change
        result = [ ]
        for item in self.music:
            if isinstance(item, Note):
                item = Note(value=item.value+amount,length=item.length,bar_position=item.bar_position)
            result.append(item)
        self.music = result
        self.final += amount
                
    #TODO: remember to use unit to render output
    
    #TODO: P can just stand as a marker, get reinserted in output -- whole piece has one tonality
        


def read_songs(file):
    def parse():
        try:
            songs.append(Song(result))
        except Error, error:
            print >> sys.stderr, "Error:", error.args[0]

    result = [ ]
    songs = [ ]
    for line in file:
        line = line.strip()
        if not line:
            if result:
                parse()
                result = [ ]
        else:
            result.append(line)
    if result:
        parse()
    return songs



def write_set_state(letter, value, state):    
    if state[letter] == value:
        return ""
        
    state[letter] = value
    if value == -1:
        return "_"
    elif value == 0:
        return "="
    else:
        return "^"

    
def shatter(music):
    result = [ ]
    
    for item in music:
        if isinstance(item, Note):
            unit = item.shatter
            while item.length > unit:
                result.append(item.but_with(length=unit, slur=False))
                item = item.but_with(length=item.length-unit, bar_position=item.bar_position+unit)
            result.append(item.but_with(slur=False))
        else:
            result.append(item)
            
    return result
    
def join_up(music):
    result = [ ]
    for item in music:
        if not result or \
           not isinstance(result[-1], Note) or \
           not isinstance(item, Note) or \
           result[-1].value != item.value or \
           item.bar_position.multiple_of(item.emphasis):
            result.append(item)
        else:
           result[-1] = result[-1].but_with(length=result[-1].length+item.length)
       
    music = result       
    result = [ ]
    for item in music:
        if not result or \
           not isinstance(result[-1], Note) or \
           not isinstance(item, Note) or \
           result[-1].value != item.value or \
           (not result[-1].bar_position.multiple_of(result[-1].emphasis) or 
            not item.bar_position.multiple_of(item.emphasis) or
            not item.length.multiple_of(item.emphasis)):
            result.append(item)
        else:
           result[-1] = result[-1].but_with(length=result[-1].length+item.length)
           
    return result
