  
"""Sequence, Event and Phrase classes"""

import threading

from . import seqgens

def list_methods_to_pitches(cls):
    """Inject list methods into Sequence such that they apply
        to the "pitches" attribute."""
    overrides = ('__repr__',  '__ge__','__gt__','__lt__',
             '__le__','__eq__', '__ne__')
    for name, meth in vars(list).items():
        if not hasattr(cls, name) or name in overrides:
            def new_meth(self, *args, meth=meth):
                reference = self.pitches[:]
                result = meth(reference, *args)    
                if reference != self.pitches:
                    self.__init__(*reference, cardinal=self.cardinal)
                if isinstance(result, list):
                    return cls(*result, cardinal=self.cardinal)
                return result
            new_meth.__name__ = name
            new_meth.__doc__ = meth.__doc__    
            setattr(cls, name, new_meth)
    return cls

@list_methods_to_pitches        
class Sequence:
    """Sequence, a representation of sequences of numbers either as absolute
        values or as "step" intervals, with a modulo value called "cardinal".
        Its methods return various characteristics of the sequence."""
        
    #Stores values written by leaders and read by followers:
    master_dict = dict.fromkeys(('variety', 'open_scale', 'is_mirror',
        'consonance', 'symmetry', 'closed_scale', 'z_depth', 'z_variety'))

    def __init__(self, *sequence, steps=False, cardinal=12):
        """doc"""

        if not all(isinstance(i, int) for i in sequence):
            raise ValueError('Arguments must be integers')
        if steps:
            #Sequence is input as consecutive intervals
            sequence = [sum(sequence[:n]) for n in range(len(sequence) + 1)]    
        if sequence:
            pcs = sorted(set([i % cardinal for i in sequence]))
            steps = ([pcs[n] - pcs[n - 1] for n in range(1, len(pcs))]
                + [cardinal + pcs[0] - pcs[-1]])
            p_steps, offset = min((steps[i:] + steps[:i], pcs[i % len(pcs)]) 
                for i, j in enumerate(steps, 1) if j == max(steps))
        else:
            p_steps, offset = [], 0
        prime = [sum(p_steps[:n]) for n in range(len(p_steps))]
        self.order = [prime.index(j) + i * len(prime) for i, j in
                    (divmod(i - offset, cardinal) for i in sequence)]
        self.__prime_steps = p_steps
        self.__cardinal = cardinal    
        self.__offset = offset    
        self.__prime = prime

    @property
    def cardinal(self):
        """doc"""
        return self.__cardinal

    @property
    def prime(self):
        """doc"""
        return self.__prime

    @property
    def prime_steps(self):
        """doc""" 
        return self.__prime_steps
        
    @property
    def offset(self):
        """doc""" 
        return self.__offset

    def __stepify(self, seq):
        """doc"""
        return [seq[n + 1] - seq[n] for n in range(len(seq) - 1)]

    #Contingent properties

    @property    
    def pitches(self):
        """The actual notes"""

        return [self.prime[j] + i * self.cardinal + self.offset for i, j in
                (divmod(i, len(self.prime)) for i in self.order)]
            
    def pcset(self):
        """The pitch-class set"""
        return sorted ([(i + self.__offset) % self.cardinal
            for i in self.prime])

    def norm(self):
        """The normal form"""
        pcs = self.pcset()
        mini = pcs[0]
        return [i - mini for i in pcs]

    def unique_pitches(self):
        """Unique sorted pitches"""
        return sorted(set(self))

    def full_steps(self):
        """Unique sorted pitches"""
        if self:
            span = max(self) - min (self)
            octs = (span // self.cardinal + 1) * self.cardinal
            return self.steps() + [octs - span]
        return []       

    def steps(self):
        """Intervals between the unique sorted pitches"""
        return self.__stepify(self.unique_pitches())
        
    def pcsteps(self):
        """The intervals in the normal form"""
        return self.__stepify(self.pcset())

    def all_steps(self):
        """All intervals up and down the pitches"""
        return self.__stepify(self)

    def is_prime(self):
        """doc"""
        return self == self.prime


    def __compute_once(meth):
        """Inherent attributes should ony be computed once"""
        att = '__' + meth.__name__
        def compute_once(self):
            """Only compute attribute if it is not present"""
            if not hasattr(self, att):
                setattr(self, att, meth(self))
            return getattr(self, att)
        return compute_once

    #Inherent properties

    @property
    @__compute_once
    def variety(self):
        """How many different step sizes"""
        return len(set(self.prime_steps))

    @property
    @__compute_once
    def open_scale(self):
        """No consecutive semitones"""
        prime = self.prime_steps
        return not any(prime[i]  == prime[i - 1] == 1
                                    for i in range(len(prime)))

    @property
    @__compute_once
    def closed_scale(self):
        """Like open_scale but also has no room for more notes"""
        prime = self.prime_steps
        length = len(prime)
        return self.open_scale and not any(prime[i] > 3 or
                        (prime[i] == 3 and
                        (prime[i - 1] > 1 or prime[(i + 1) % length] > 1))
                        for i in range(length))

    @property
    @__compute_once
    def inv_prime(self):
        """doc"""
        if self:
            spets = list(reversed(self.prime_steps))
            maxi = max(spets)
            rotations = (spets[i:] + spets[:i] for i in range(len(self.prime)))
            return min(i for i in rotations if i[-1] == maxi)
        return []
            
    @property
    @__compute_once
    def antiprime_steps(self):
        """doc"""
        if self:
            steps = self.prime_steps   
            return max(steps[i:] + steps[:i] for i, j in enumerate(steps, 1)
                    if j == min(steps))
        return []

    @property
    @__compute_once
    def antiprime(self):
        """doc"""    
        return [sum(self.antiprime_steps[:n])
                 for n in range(len(self.prime))]
           
    @property
    @__compute_once
    def is_forte(self):
        """doc"""
        return self.prime_steps <= self.inv_prime

    @property
    @__compute_once
    def is_mirror(self):
        """doc"""
        return self.prime_steps == self.inv_prime

    @property
    @__compute_once
    def symmetry(self):
        """How many symmetries in the step structure"""
        ps = self.prime_steps
        return [ps[n:] + ps[:n] for n in range(len(ps))].count(ps) - 1

    #Z-vector properies

    @property
    @__compute_once
    def vector(self):
        """Returns the Z-vector of the pc-set"""
        pcs = self.prime
        cardinal = self.cardinal
        z_vector = [0] * (cardinal // 2)
        for i, j in enumerate(pcs):
            for k in pcs[ i + 1:]:
                interval = k - j
                z_vector[min(interval, cardinal - interval) - 1] += 1
        return tuple(z_vector)

    @property
    @__compute_once
    def z_variety(self):
        """How many different interval types"""
        return len([i for i in self.vector if i])

    @property
    @__compute_once
    def z_depth(self):
        """How many different interval counts"""
        return len(set(i for i in self.vector if i))

    @property
    @__compute_once
    def consonance(self):
        """A measure of consonance"""
        vector = self.vector
        return sum(vector[2:5]) - sum(vector[:2]) - vector[-1]

    #actions:


    def transpose(self, num):
        """Increment all values by the same amount"""
        octinc, self.__offset = divmod(num + self.__offset, self.cardinal)
        self.order = [i + octinc * len(self.prime) for i in self.order]

    def voice(self, index, num):
        """Raise or lower an element by a number of octaves"""
        self.order[index] += num * len(self.prime) 
        
    def sort(self, **kwargs):
        """Sort the pitches"""
        self.order.sort(**kwargs)

        
    def reverse(self):
        """Reverse the order of pitches"""
        self.order.reverse()

    def emult(self, n):
        """Multiply all elements by an integer"""
        self.__init__(*[i * n for i in self], cardinal=self.cardinal)
        
    def inverted(self):
        """Reverses the order of the intervals, starting from the same note."""
        if self:
            s = self.__class__(*reversed(self.all_steps()), cardinal=self.cardinal, steps=True)
            s.transpose(min(self))
            return s
        return []      


class SeqGen():
    """doc"""
    def __init__(self, optdict, moddict):
        """doc"""
        self.optdict = optdict
        self.moddict = moddict
        self._gen = self._squiterate()
        self.counter = {}
        
    def __repr__(self, lvl=0):
        string = '\n' + str(self.__class__.__name__) + ': '
        lvl += 1
        for arg in self.optdict, self.moddict:
            string += '\n' + lvl * '    '
            if arg:
                for k, v in arg.items():
                    if isinstance(v, self.__class__):
                        string += k.__name__ + ': ' + v.__repr__(lvl)
                    else:
                        string += str(arg) 
            else:
                string += 'defaults'
        return string
        
    def seq(self):
        "Number sequences to iterate through. Repeatable."
        seq = self.optdict.get(self.__class__.seq)
        if seq:
            return iter([next(seq)])

    def generator(self, cardinal):
        """Which number generators to use. Currently 'full',
        'combs', or 'randgen'. Controllable."""
        gengen = self.optdict.get(self.__class__.generator)
        if gengen:
            gens = [i for i in dir(seqgens) if i[0] != '_']
            gen = gens[next(gengen) % len (gens)]
            return getattr(seqgens, gen)(cardinal)
        else:
            return self.seq() or seqgens.full(cardinal)

    def cardinal(self):
        "The number (of notes/beats) to be processed."
        cardgen = self.optdict.get(self.__class__.cardinal)
        if cardgen:
            for _ in range(100):
                cardinal = next(cardgen)
                if cardinal:
                    return cardinal
            else:
                raise StopIteration
        else:
            return 12

    def mortal(self):
        "Generator does not loop but dies when complete. Boolean."
        mort = self.optdict.get(self.__class__.mortal)
        return mort and next(mort)

    def repeat(self):
        "How many times to repeat sequence."
        rep = self.optdict.get(self.__class__.repeat)
        return rep and next(rep) or 1

        
    def _modhandle(self, seq):
        """Ensure the required number of sequences have been yielded
        been applications of a filter or modification """
        counter = self.counter
        for func, gen in self.moddict.items():
            if func not in counter:
                counter[func] = [next(gen), 1]        
            result = None
            if 'Boolean' in func.__doc__:
                if counter[func][1] >= counter[func][0]:
                    result = func(seq)                 
            else:  
                result = func(seq, counter[func][0])
            if result is False:
                break               
        else:
            for func, gen in self.moddict.items():
                value = counter[func]
                if 'Boolean' in func.__doc__:
                    if value[1] >= value[0]:
                        value[1] = 0
                        value[0] = next(gen)               
                    value[1] += 1
                else:
                    value[0] = next(gen)   
            return True
    

    def _squiterate(self):
        """doc"""
        empty = 0
        while True:
            cardinal = self.cardinal()                             
            for seqlist in self.generator(cardinal):
                seq = Sequence(*seqlist, cardinal=cardinal)
                if self._modhandle(seq):             
                    empty = 0
                    for _ in range(self.repeat()):
                        yield seq
            if self.mortal():
                break
            empty += 1
            if empty == 100:
                print("""One hundred consecutive empty generators:
                        broaden filters.""")
                        
    def __next__(self):
        """doc"""
        return next(self._gen)

    def __iter__(self):
        """doc"""
        return self


class Event():
    """Events produced within phrase_builder"""
    def __init__(self, pitches=[0], start=0, playtime=None, duration=1, 
            volume=10, tempo=250, octave=12, instrument=0, pedal=False):

        self.__dict__.update({k: v for k, v in vars().items()
                                if k != 'self'})

    def __repr__(self):
        return str(vars(self))
        
    def __len__(self):
        return len(self.pitches)

class PhraseMaker():
    """Phrase building options"""
    def __init__(self, gendict):
        self.gendict = gendict
        self._gen = self._phriterate()
        self.length = None
        self.boolcounter = {}
        
    def __repr__(self):
        string = str(self.__class__.__name__) + ':'
        if self.gendict:            
            for k, v in self.gendict.items():
                string += '\n   ' +  k.__name__ + ': ' + v.__repr__()
            return string    
        else:
            return string + '\n   defaults'    
        
                
    def _boolcount(meth):
        """Return True if we have yeilded the currently
        required number of Phrases"""
        def wrapper(self):
            key = getattr(self.__class__, meth.__name__)
            if key in self.gendict:
                if key not in self.boolcounter:
                    self.boolcounter[key] = [next(self.gendict[key]), 0]
                self.boolcounter[key][1] += 1
                if self.boolcounter[key][0] == self.boolcounter[key][1]:
                    self.boolcounter[key] = [next(self.gendict[key]), 0]
                    return meth(self)
        wrapper.__name__ = meth.__name__
        wrapper.__doc__ = meth.__doc__        
        return wrapper        
            
    def _set_len(self, key):
        """The first parameter's value sets the length of the others;
        the parameters are returned"""
        gen = self.gendict[key]
        result = next(gen)
        if self.length:
            while len(result) < self.length:
                result += next(gen)
        else:
            self.length = len(result)
        return result

    def pitches(self):
        """Space-separated number sequence giving
        the pitch of each note. Repeatable."""
        key = self.pitches.__func__
        if self.chord():
            if self.length:
                return [next(self.gendict[key])
                    for i in range(self.length)]
            self.length = 1
            return [next(self.gendict[key])]   
        return [[i] for i in self._set_len(key)]

    def duration(self):
        """Step values determining the duration of each note."""
        return self._set_len(self.duration.__func__)
     
    def playtime(self):
        """Step values determining how long to hold each note."""
        return self._set_len(self.playtime.__func__)
        
    def staccato(self):
        """How many twelfths of the duration to hold note for. Repeatable."""
        key = self.staccato.__func__
        if key in self.gendict:
            return [i / 12 for i in next(self.gendict[key])]
        return [1]
        
    def volume(self):
        "Volume, 0-10. Default 10. Repeatable."
        return self._set_len(self.volume.__func__)       
         
 
    def tempo(self):
        "Tempo in BPM."
        for _ in range(100):
            tempo = next(self.gendict[self.tempo.__func__])
            if tempo:
                return [15000.0 // tempo]
        raise StopIteration
        
    def octave(self):
        "The number to divide the octave by."
        for _ in range(100):
            octave = next(self.gendict[self.octave.__func__])
            if octave:
                return [octave] 
        raise StopIteration
              
    @_boolcount    
    def chord(self):
        "Play sequences as chords. Boolean."
        return True
            
    @_boolcount
    def pedal(self):
        "Let pitches ring out. Boolean."
        return [True]    

    def instrument(self):
        "MIDI program number."
        return [next(self.gendict[self.instrument.__func__ ])]

    def __iter__(self):
        return self

    def __next__(self):
        return next(self._gen)

    def _phriterate(self):
        "Generate phrases"
        start = 0
        eventvars = Event.__init__.__code__.co_varnames
        while True:
            eventdict = {func.__name__: func(self)
                for func in self.gendict if func.__name__ in eventvars}   
            phrase = [Event(**{k: val[i % len(val)] 
                        for k, val in eventdict.items()})
                        for i in range(self.length or 1)]
            staccatos = self.staccato()
            for i, event in enumerate(phrase):
                event.start = start
                start += event.duration * event.tempo
                event.playtime = ((event.playtime or event.duration) *
                                    staccatos[i % len(staccatos)])
            self.length = None
            yield phrase
         

class IterNode():
    "Iterator wrapper to give access to argument"
    def __init__(self, genfunc, *args):
        self.args = args
        self.genfunc = genfunc
        self.gen = genfunc(*args)
        self.__name__= genfunc.__name__
 
    def __iter__(self):
        return self

    def __next__(self):
        "doc"
        return next(self.gen)

    def __repr__(self, lvl=0):
        string = str(self.genfunc.__name__) + ': '
        lvl += 1
        for arg in self.args :
            string += '\n' + lvl * '    '
            if isinstance(arg, dict):
                if arg:
                    for k, val in arg.items():
                        string += k.__name__ + ': ' + val.__repr__(lvl)
                else:
                    string += 'defaults'   
            else:
                string += str(arg)
        return string
 
    def deepdate(self, other):
        "Deep-update a nested IterNode"
        if self.genfunc.__name__ != other.genfunc.__name__:
           self.__init__(other.genfunc, *other.args)
        else:
            for selfarg, otherarg in zip(self.args, other.args):
                if (not all(isinstance(i, dict) for i in (selfarg, otherarg))
                    and selfarg != otherarg):
                    self.__init__(other.genfunc, *other.args)
                    break
                elif selfarg != otherarg:
                    for k in selfarg:
                        if k not in otherarg:
                            del selfarg[k]
                    for k, val in otherarg.items():
                        if k in selfarg:
                            selfarg[k] = selfarg.pop(k)
                            selfarg[k].deepdate(val)
                        else:
                            selfarg[k] = val
