
# coding: utf-8

# # Westergaard Species Counterpoint Utils

# ## Imports

# Standard library imports:

# In[ ]:

import copy
import re
import operator


# External packages imports:

# In[ ]:

import yaml
from iabjad import isIPython
if isIPython():
    from iabjad import export, export_, ishow, ishow_


# Abjad imports:

# In[ ]:

from abjad import *


# ## Input Parsing

# ### File Format
# 
# Score analysis will be described by a YAML file.
# YAML files are human readable and the syntax is easy to learn.
# 
# An example file is (note that this is valid YAML, including the comments):
# 
# <hr/>
# 
# ```yaml
# key: c major
# #  Key signature for the score.
# #  It follows the syntax
# #      <pitch> <mode>
# #  Supported pitch names are all valid LilyPond note names.
# #  Supported mode names are 'major', 'minor' and the
# # standard church modes.
# 
# layout:
# # Settings related to the typesetting of the music.
# # They have sane defaults and you only need to
# # specify them if you have speciall needs
#     line-width: 70 # line width of the score in points
#     staff-size: 16 # staff size
# 
# lines:
# #  A list of simultaneous music lines.
# #  Any number of lines is supported.
# #  This example contains 3 lines,
# # named 'upper' and 'lower'
#     - name: upper # name of the line
#       clef: treble
#       #  Clef for the line.
#       #  'treble', 'bass' and 'tenor' are supported.
#       #  Changing clefs in the middle of the line
#       # is not supported.
#       notes: e'1 c' f' e' g' f' e' d' c'
#       # Sequence of notes for the line.
#       # You will have to specify the rhythm of the notes.
#       # Note that the note names are absolute and not relative
#     - name: lower
#       clef: bass
#       notes: c1 e d g e f g g c
# 
# stages:
# # Stages in which the line is generated.
# # There can be any number of stages.
#     - upper: _|1; _|8; _|9
#       #  List of rules for the upper line,
#       # named and described above.
#       #  There can be any number of rules,
#       # generating any number of notes.
#       #  In this case, _ (underscore) rules
#       # are used. The _ rule is a rule with no
#       # name. As such, no rule name will appear
#       # above the notes generated by this rule.
#       #
#       #  A rule of the form "R1|i" means that
#       # note number i (indexing starts at one)
#       # was generated according to rule R1.
#       #  Rules are separated by ; (colons) 
#       lower: _|1; _|7; _|9 
#       # List of rules for the lower line.
#     
#     - upper: ""
#       #  A line with no rules must be written as "".
#       #  This is needed to make the file a valid YAML
#       # file.
#       lower: B3|2
#     
#     - upper: B1|4
#       lower: B1|5; B1|8
#       # The same rule may appear more than once in the
#       # same line.
#     
#     - upper: B2|3; B3|5
#       lower: B2|3; B4|6
#       # Different rules may appear in the same line more
#       # than once.
#     
#     - upper: B3|2; B4|6,7
#       #  A single rule may generate more than one note.
#       #  A rule of the form "R1|i,j,k" means that notes
#       # number i, j and k were generated according to
#       # rule R1
#       lower: B3|4
# ```
# 
# <hr/>
# 
# While the file may appear exceedingly verbose, note that the structure is very clear and after removing the comments and the *layout* part (which is usually not needed) the file is quite short.
# 
# The next sections deal with parsing this file format into an ``abjad.Score`` that can be typeset and analysed.

# ### Key Signature
# 
# The key signature format is very simple:
# 
# <p style='text-align:center'>``key:`` *tonic* *mode*<p>
# 
# It is simply the name of the *tonic* followed by any amount of whitespace, followed by the name of the *mode*.
# *tonic* can be any
# [pitch](http://www.lilypond.org/doc/v2.18/Documentation/notation/writing-pitches#note-names-in-other-languages) 
# accepted by LilyPond when configured for the english language (which is not  (e.g. ``a``, ``cf``, ``es``, etc.)
# and *mode* can be one of ``major``, `` minor`` or one of the
# [church modes](http://www.lilypond.org/doc/v2.18/Documentation/music-glossary/diatonic-scale)
# (``ionian``, ``dorian``, ``phrygian``, ``lydian``, ``myxolidian``, ``aeolian``, ``locrian``).
# 
# This format taken by the value of the ``key`` item is described by the following regular expression. Note that the expression does not try to validate the *tonic* or *mode* names. The validations is done by the ``parse_key`` function.

# In[ ]:

key_regexp = r"(\s)*(?P<tonic>(\w)*)(\s)*(?P<mode>(\w)*)"


# The ``parse_key`` function generates an ``abjad.KeySignature`` using the regular expression above.

# In[ ]:

def parse_key(line):
    m = re.match(key_regexp, line)
    tonic = m.group('tonic')
    mode = m.group('mode')
    return KeySignature(tonic, mode)


# ### Clef
# 
# The clef is chosen from the line: "``clef: `` *name*" using the function ``parse_clef``.

# In[ ]:

def parse_clef(clef_name):
    return Clef(name=clef_name)


# ## Rules

# A rule is a pairs containing the rule name and the index (indices) of the note (notes) generated by the rule.
# the general format is:  *rulename*``|``*indices*, where:
# 
# - *rulename* is either the character ``_`` (underscore), for an unnamed rule or an arbitrary string not containing the ``|`` (pipe) character
# 
# - *indices* is a comma separated list of integers. The numbering starts at 1 (one). This is more intuitive to users but will have to be accounted for in the code, given that the numbering in python sequences starts at 0 (zero).
# 
# Some examples of well formed rules:
# 
# - ``A1|1`` - rule **A1** generates note number **1**.
# - ``B4|1,2,3`` - rule **B1** generates notes numbered **1**, **2** and **3**.
# - ``A3|1,3,4`` - rule **A3** generates notes numbered **1**, **3** and **4**. Note that the indices do not have to be contiguous integers.
# - ``_|4`` - unnamed rule generates note number **4**. Note number **4** will appear without any rule attached to it.
# 
# The corresponding regular expression to parse a single rule is:

# In[ ]:

rule_regexp = r"(\s)*(?P<name>.*)(\s)*\|(\s)*(?P<rest>.*)"


# The function ``parse_rule`` parses a single rule.
# Note that the indices of the notes generated by the rule are corrected to be 0-based instead of 1-based.

# In[ ]:

def parse_rule(rule):
    rule_m = re.match(rule_regexp, rule)
    name = rule_m.group('name')
    #  A rule name which is an underscore will be used to
    # represent a note that appears according to a rule we
    # do not name (a rule with an empty name)
    if name == '_':
        name = ""
    # The 'rest' part of the rule is a comma separated
    # list of integers.
    rest = rule_m.group('rest')
    indices = rest.strip().split(',')
    #  No need to strip th indices, because int() recognizes
    # a number surrounded by whitespace
    return {'name': name,
            # tranform indices into a 0-based system:
            'indices': map(lambda x: int(x) - 1, indices)}


# The function ``parse_rules`` parses a list of rules spearated by semicolons (``;``):

# In[ ]:

def parse_rules(line):
    rules = filter(lambda x: x, line.strip().split(';'))
    return map(parse_rule, rules)


# ### Parsing the YAML File
# 
# The yaml library makes parsing YAML files into a python dictionary very easy.
# We call that dictionary *steps* because they explain the steps in which a line can be understood.
# 
# The dictionary is mostly created by the ``yaml.safe_load()`` function.
# Parts of the dictionary are then processed from ``strings`` into more advanced types, namely
# ``KeySignature``,
# ``Clef``,
# ``Note``,
# or dicts representing the rules.

# In[ ]:

def steps_from_yaml(text):
    steps = yaml.safe_load(text)
    steps['key'] = parse_key(steps['key'])

    for i, line in enumerate(steps['lines']):
        #  Parse the notes into a proper Abjad Voice.
        #  This allows us to deal with rhythms, including tuplets and ties,
        # which we need for higher species.
        steps['lines'][i]['voice'] = Voice(line['notes'])
        # The string containing the notes is not needed anymore.
        del steps['lines'][i]['notes']
        
        steps['lines'][i]['clef'] = parse_clef(line['clef'])

    for i, stage in enumerate(steps['stages']):
        for line_name, rules in stage.iteritems():
            steps['stages'][i][line_name] = parse_rules(rules)
    
    return steps


# The following function generates a score from a YAML file using a function defined below.

# In[ ]:

def score_from_yaml(text, has_bars=False):
    steps = steps_from_yaml(text)
    return score_from_steps(steps, has_bars)


# ## Creating the Score

# In[ ]:

def skip_with_same_duration_as(entity):
    return scoretools.Skip(entity.written_duration)


# In[ ]:

midHB_def_label = """
midHB =
#(define-music-function (parser location text) (markup?)
#{
  \once \override Staff.HorizontalBracket #'stencil = #(lambda (grob)
    ;; adapted from LSR snippet, "Center text below hairpin dynamics"
    (ly:stencil-aligned-to
      (ly:stencil-combine-at-edge
        (ly:stencil-aligned-to (ly:horizontal-bracket::print grob) X CENTER)
        Y (ly:grob-property grob 'direction)
            (ly:stencil-aligned-to (grob-interpret-markup grob
                                 (markup text)) X CENTER)
      0.5)
     X LEFT))
#})
"""


# Adding a rule to the staff is one of the most important operations in the program.
# There are two kind of rule applications:
# 
# 1. Rule applications that add a single note to the staff (e.g. ``A1|7``); we'll call this situation a *single-note insertion*
# 2. Rule applications that add more than one note to the staff (e.g. ``A3|2,3,4``); we'll call this situation a *multi-note insertion*.
# 
# Single-note inserions are easy to handle. One needs only add the name of the rule as markup to the note being added, with the command ``abjad.attach(``*name*``, staff[``*index*``])``, and LilyPond takes care of the positioning.
# 
# Multi-note insertions are also easy, but need the ``\midHB`` command defined in the LilyPond file.

# In[ ]:

def centered_rule_name(name, style='italic'):
    # Invoke the \midHB command we defined somewhere else in the LilyPond file.
    cmd_str = r"midHB \markup {\whiteout \pad-markup #0.3 {\%s {%s}}}" % (style, name)
    #  \whiteout creates a white rectangle around the markup.
    #  This is useful when the rule name crosses a staff bar,
    # and has no effect in other situations.
    return indicatortools.LilyPondCommand(cmd_str, 'before')

def add_rule(staff,
             name,
             indices, # Must be either a single integer or a pair (not a list!)
                      # of integers that works like a range. 
             style="italic"):
    
    # There is no default type for text, so we use the
    # normalsize style, which is the "default" one.
    if style is None:
        text_style = "normalsize"
    else:
        text_style = "italic"
    
    staff_leaves = staff.select_leaves()
    
    # A spanner is something the 'spans' multiple notes
    if isinstance(indices, tuple) and len(indices) == 2:
        left, right = indices
        spanner = spannertools.HorizontalBracketSpanner()
        attach(spanner, staff_leaves[left:right])
        attach(centered_rule_name(name, text_style), staff_leaves[left])

    # Single-note insertions:
    elif isinstance(indices, int):
        index = indices
        text = markuptools.MarkupCommand(text_style, [name])
        markup = markuptools.Markup(text, direction=Up)
        attach(markup, staff_leaves[index])
    else:
        raise Exception("Invalid Index! Must be either 'tuple' or 'int'")


# In[ ]:

def check_line_durations(lines):
    durations = map(lambda line: inspect_(line['voice']).get_duration(), lines)
    same_duration = reduce(operator.eq, durations)
    if same_duration:
        return durations[0]
    else:
        print "Unequal Durations"
        for line in lines:
            print "- line named '{}':".format(line['name'])
            print "    voice: {!s}".format(line['voice'])
            print "    duration: {!s}".format(inspect_(line['voice']).get_duration())
        raise Exception("Unequal Durations")


# In[4]:

def score_from_steps(steps, has_bars=False, final_bar_line="|."):
    key = steps['key']
    
    staves_per_stage = len(steps['lines'])    
    nr_of_stages = len(steps['stages'])

    #  Line durations are always checked.
    #  If you want lines with different durations, insert skips
    # like you'd do with LilyPond
    total_duration = check_line_durations(steps['lines'])
    
    if has_bars:
        time_signature = TimeSignature((1,1))        
    else:
        # To avoid having to remove bar lines,
        # choose a time signature such that all
        # the music fits in a single compass.
        time_signature = TimeSignature(total_duration)
    
    score = Score()
    for stage_nr, stage in enumerate(steps['stages']):
        outer_group = StaffGroup()
        inner_group = StaffGroup()
        for line_nr, line in enumerate(steps['lines']):
            
            voice = copy.deepcopy(line['voice'])
            
            staff = Staff(list(voice))
            leaves = staff.select_leaves()
            
            for i, note in enumerate(voice.select_leaves()):
                mutate(leaves[i]).replace(skip_with_same_duration_as(note))
            
            staff.consists_commands.append('Horizontal_bracket_engraver')
            staff.remove_commands.append('Time_signature_engraver')
            attach(key, staff)
            attach(line['clef'], staff)
            override(staff).horizontal_bracket.direction = Up
            inner_group.append(staff)

        attach(copy.deepcopy(time_signature), inner_group[0], scope=Staff)
            
        override(outer_group).system_start_bracket.style = 'bar-line'
        override(outer_group).system_start_bracket.thickness = 1.6
        override(outer_group).system_start_bracket.padding = 0
        
        override(inner_group).system_start_bracket.style = 'bracket'
        override(inner_group).system_start_bracket.thickness = 0.5
        override(inner_group).system_start_bracket.padding = 0.8

        outer_group.append(inner_group)
        score.append(outer_group)
        
    for stage_nr, stage in enumerate(steps['stages']):
        for line_nr, line in enumerate(steps['lines']):
            rules = stage[line['name']]
            for rule in rules:
                rule_name = rule['name']
                indices = rule['indices']
                for group in score[stage_nr:]:
                    
                    staff_leaves = group[0][line_nr].select_leaves()
                    voice = copy.deepcopy(line['voice'])
                    voice_leaves = voice.select_leaves()
                    
                    for index in indices:
                        mutate(staff_leaves[index]).replace(voice_leaves[index])
                # Tansform indices into a range or an int
                if len(indices) >= 2:
                    # A range is a pair of ints
                    rule_indices = (min(indices), max(indices)+1)
                elif len(indices) == 1:
                    rule_indices = indices[0]
                # Add the rule:
                add_rule(score[stage_nr][0][line_nr], rule_name, rule_indices)
    
    #  Hide the leftmost bar line of the system, so that our system
    # looks like a series of independent systems that happen to be
    # vertically aligned.
    override(score).system_start_bar.stencil = None
    #  Render tuplet brackets and numbers under the staff
    # to avoid clashes with the rule names or horizontal bars.
    override(score).tuplet_bracket.direction = Down
    
    if final_bar_line:
        score.add_final_bar_line(final_bar_line)
    else:
        override(score).bar_line.break_visibility = schemetools.Scheme('end-of-line-invisible')
        
    return score


# ### Generating a LilyPond file from a YAML file
# 
# By default, lilypond tries to typeset the score into an [A4-sized](http://en.wikipedia.org/wiki/Paper_size#A_series) sheet of paper. This is sometimes too small to make the music fit in a single page. This can't be set from the ``Score``, which makes it neccessary to generate a lilypond file instead of only score.
# 
# This allows us to control a great deal of the layout of the score, including:
# 
# - [paper size](http://lilypond.org/doc/v2.18/Documentation/notation/predefined-paper-sizes)
# - [line width](http://lilypond.org/doc/v2.18/Documentation/notation/line-length) of the score, to make it wither narrower or wider than the default.
# - [staff size](http://lilypond.org/doc/v2.18/Documentation/notation/setting-the-staff-size), possibly in combination with the above
# - does the score have bars?
# - what's the offset of the text with or without bars?

# In[2]:

def lilypondfile_from_yaml(text, size='a3'):
    
    line_width = None
    staff_size = None
    has_bars = False
    final_bar_line = "|."
    
    steps = steps_from_yaml(text)
    
    try:
        layout = steps['layout']
        try: line_width = layout['line-width']
        except: pass
        
        try: staff_size = layout['staff-size']
        except: pass
        
        try: has_bars = layout['has-bars']
        except: pass
        
        try: final_bar_line = layout['final-bar-line']
        except: pass
        
    except:
        pass
    
    score = score_from_steps(steps, has_bars, final_bar_line)
        
    lilypond_file = lilypondfiletools.make_basic_lilypond_file(score)
    lilypond_file.default_paper_size = size, 'portrait'
    # Insert the definition of the \midHB command as a raw string
    lilypond_file.items.insert(0, midHB_def_label)
    lilypond_file.header_block.tagline = None
    if line_width is not None:
        lilypond_file.layout_block.line_width = line_width
    if staff_size is not None:
        lilypond_file.global_staff_size = staff_size
    return lilypond_file


# ### Convenience Functions
# 
# These functions allow you to export or show the score directly from a YAML string.

# In[ ]:

if isIPython():
    def ishow_from_yaml(yaml, *args, **kwargs):
        lilyfile = lilypondfile_from_yaml(yaml)
        ishow(lilyfile, *args, **kwargs)

    def ishow_preview_from_yaml(yaml, *args, **kwargs):
        lilyfile = lilypondfile_from_yaml(yaml)
        ishow_(lilyfile, *args, **kwargs)

    def ishow_from_yaml_(yaml, *args, **kwargs):
        lilyfile = lilypondfile_from_yaml(yaml)
        ishow_(lilyfile, *args, **kwargs)


# In[ ]:

if isIPython():
    def export_from_yaml(yaml, *args, **kwargs):
        lilyfile = lilypondfile_from_yaml(yaml)
        export(lilyfile, *args, **kwargs)

    def export_preview_from_yaml(yaml, *args, **kwargs):
        lilyfile = lilypondfile_from_yaml(yaml)
        export_(lilyfile, *args, **kwargs)

    def export_from_yaml_(yaml, *args, **kwargs):
        lilyfile = lilypondfile_from_yaml(yaml)
        export_(lilyfile, *args, **kwargs)


# ##[Apendix](http://nbviewer.ipython.org/github/cryptonomicon314/westergaard_species_demo/blob/master/appendix.ipynb)
# 
# - [Examples of Westergaard Species Counterpoint analyses](http://nbviewer.ipython.org/github/cryptonomicon314/westergaard_species_demo/blob/master/appendix.ipynb#A.1.-Examples)
# - [Supported Pitches and Accidentals](http://nbviewer.ipython.org/github/cryptonomicon314/westergaard_species_demo/blob/master/appendix.ipynb#A.2-Pitches-and-Accidentals)
# - [Supported Clefs](http://nbviewer.ipython.org/github/cryptonomicon314/westergaard_species_demo/blob/master/appendix.ipynb#A.2.-Clefs)
