from fractions import Fraction
from functools import reduce

ONSET = (1, )
TIE = (0, )

def is_onset(node):
    return node == ONSET

def is_tie(node):
    return node == TIE

def metrical_weight_vector(meter, head=0, lvl=0):
    '''Take a list of subdivisions. 
    Return a list of metrical weights.
    '''

    try:
        divisor, *submeter = meter
    except ValueError:
        return [head]

    lvl -= 1
    met_sal = metrical_weight_vector(submeter, head=head, lvl=lvl)

    for i in range(1, divisor):
        met_sal += metrical_weight_vector(submeter, head=lvl, lvl=lvl)

    return met_sal

def syncopation(note_weight, rest_weight):

    return (0 if note_weight > rest_weight else
            rest_weight - note_weight)

def pretty_print_tree(node, level=0):

    try:
        label, children = node
    except ValueError:
        label = node
        children = []

    for whatever in range(level):
        print('\t', end='')

    print(label)
    for child in children:

        pretty_print_tree(child, level=level+1)

def _get_child_nodes(node):
    '''
    Retrieve the all child nodes of a subtree of intermediate nodes
    generated by a grammar converted to CNF.
    '''

    try:
        label, (left, right) = node
    except ValueError:
        return [node]

    children = [left] + (
        _get_child_nodes(right) if len(right[0]) == 3 else
        [right]
    )

    return children

def cnf_to_normal(node):
    '''
    Convert a tree generated by a grammar that was converted to CNF to 
    it's original form
    '''

    try:
        label, (left, right) = node
    except ValueError:
        return node 

    children = [left] + (
        _get_child_nodes(right) if len(right[0]) == 3 else
        [right]
    )
                        
    return (label, tuple([cnf_to_normal(child) for child in children]))

def produces_terminal(node):

    label, children = node

    return len(children) == 1

FEATURES = ('initiated_by_onset', 'produces_onset')

def get_node_features(node, use_features=FEATURES):

    label, children = node

    features = {
        'initiated_by_onset':False,
        'produces_onset':False
    }

    # Does this node directly produce an onset?
    if produces_terminal(node):
        terminal, = children
        features['produces_onset'] = terminal == ONSET
        features['initiated_by_onset'] = terminal == ONSET

    elif 'initiated_by_onset' in use_features:
        # Is the leftmost child initiated by onset or does it produce an onset?
        (child_node, grandchildren), *others = children
        (child_label, child_feature_list) = child_node
        child_features = {name: value for name, value in zip(use_features, child_feature_list)}
        features['initiated_by_onset'] = child_features['initiated_by_onset']

    return tuple([features[feature] for feature in use_features])


def annotate_tree(node, **kwargs):
    '''
    Return a tree annotated with features.
    '''

    label, children = node

    # First we need to annotate the children as the current node's
    # features rely on it.

    # Stop at nodes producing a terminal
    annotated_children = children
    if not produces_terminal(node):
        annotated_children = tuple([annotate_tree(child, **kwargs) for child in children])

    features = get_node_features((label, annotated_children), **kwargs)
    annotated_label = (label, features)

    return (annotated_label, annotated_children)

def tree_to_onsets(tree, divisions=[], position=0):

    if len(tree) == 1:
        onset, = tree 
        return (
            [position] if onset == 1 else [],
            divisions
        )

    (node, children) = tree

    onsets = []
    newdivisions = []

    divisions += [len(children)] if len(children) > 1 else []
    duration = Fraction(1, reduce(lambda x, y: x*y, divisions))

    for i, child in enumerate(children):
        o, d = tree_to_onsets(child, divisions=divisions[:], position=position+i*duration)
        onsets += o

        for d_1, d_2 in zip(divisions, d):
            if d_1 != d_2:
                raise ValueError('Cannot convert to grid, time signature change detected')

        l, newdivisions = max([(len(newdivisions), newdivisions), (len(d), d)])

    return onsets, newdivisions

