# Songs are the data structure that we use for a parsed song.

class Chord(object):
    def __init__(self, name, beats):
        self.name = name
        self.beats = beats

class Line(object):
    def __init__(self):
        self.chords = [] # list of Chord objects
        self.lyrics = [] # list of String objects

class Section(object):
    def __init__(self, name):
        self.name = name
        self.lines = []  # list of Line objects

class Song(object):
    def __init__(self):
        self.all_sections = {}  # Sections objects indexed by name
        self.play_sections = [] # list of Sections, in order to be played
        self.timing_unit = 1

class ParseError(Exception):
    def __init__(self, linenum, description):
        self.description = description
        self.line_number = linenum

def parse(text):
    
    """
    Parses the text of a song file into the above data structures.
    A song file looks like:

    U: 1                          # Timing unit that each chord represents
                                  # 1 = whole, 2 = half, 4 = quarter, etc

    # Comments after a '#'
    [Chorus]                      # Section names in '[' and ']'
    C: D...                       # Line of chords begin with 'C:'
       Farmer is the one          # Lyrics to accompany last line of chords
    C: D        A             D.  # Put a '.' to extend a chord by a beat
       Yes, the farmer is the one

    [Verse-1]
    C: D...
       Sing some stuff here
            more than one line is allowed, though pointless!
    C: D        A             D.
       # no line is okay too!

    [Chorus] # second time is a reference

    [] # Anonymous sections are ok too if name is unimportant

    """

    lines = text.split('\n')
    song = Song()
    cur_section = None
    cur_line = None
    line_ctr = 0
    timing_unit = False

    for line in lines:
        line_ctr += 1

        # Strip comments
        if '#' in line:
            line = line[:line.index('#')]            

        # Check for blank lines
        stripped = line.strip()
        if not stripped:
            continue

        if stripped.startswith('U:'):
            if timing_unit:
                raise ParseError(
                    line_ctr, 'Timing unit already specified')
            if song.play_sections:
                raise ParseError(
                    line_ctr, 'Timing unit must appear before all sections')
            try:
                tu = int(stripped[2:].strip())
                if tu <= 0:
                    raise ParseError(
                        line_ctr, 'Timing unit must be at least 0')
            except ValueError:
                raise ParseError(
                    line_ctr, 'Timing unit must be a valid integer')
            
            song.timing_unit = tu
            timing_unit = True
            continue

        # Check for new section, or reference
        if stripped[0] == '[' and stripped[-1] == ']':
            section_name = stripped[1:-1]
            if section_name in song.all_sections: # already defined, ref
                song.play_sections.append(song.all_sections[section_name])
                cur_section = None
            else:
                cur_section = Section(section_name)
                if section_name:
                    song.all_sections[section_name] = cur_section
                song.play_sections.append(cur_section)
            cur_line = None
            continue

        # Otherwise, check if a section has been started
        if not cur_section:
            raise ParseError(
                line_ctr, 'No chords or lyrics allowed outside of a section')

        # Check for chords line
        if stripped.startswith("C:"):
            cur_line = Line()
            cur_section.lines.append(cur_line)
            all_chords = stripped[2:].split()
            for chordnm in all_chords:
                assert chordnm
                
                # Count and remove any '.' at the end of the chord
                # to determine how many beats it should last:
                beats = 1
                while chordnm and chordnm[-1] == '.':
                    chordnm = chordnm[:-1]
                    beats += 1

                # There must be some chord, can't be only periods
                if not chordnm:
                    raise ParseError(
                        line_ctr, 'Invalid chord "%s"' % ('.'*(beats-1)))

                # Create the Chord object:
                cur_line.chords.append(Chord(chordnm, beats))
            continue

        # Check for lyrics
        if not cur_line:
            raise ParseError(
                line_ctr, 'Lyrics without a chord!')
        cur_line.lyrics.append(line)
    return song
