 
"""Tests and modifications"""

from random import randint as _randint, shuffle as _shuffle

def _reorder(func):
    """Reorder the result of a function to match the input order"""
    def wrapper3(seq, *args):
        "As above"
        sortseq = sorted(seq)
        inds = [sortseq.index(i) for i in seq]
        func(seq, *args)
        seq.sort()
        seq.order = [seq.order[i] for i in inds]
    wrapper3.__name__ = func.__name__
    wrapper3.__doc__ = func.__doc__
    return wrapper3

def _stepwrap(func):
    """Decorator to reinitialise a sequence with new step values."""
    def wrapper4(seq, *args):
        "As above"
        if len(seq) > 1:
            base = min(seq) 
            steps = seq.all_steps()
            func(steps, *args)
            seq.__init__(*steps, cardinal=seq.cardinal, steps=True)
            seq.transpose(base - min(seq))
    wrapper4.__name__ = func.__name__
    wrapper4.__doc__ = func.__doc__            
    return wrapper4
       
###Tests

#Boolean

def openscale(seq):
    "No consecutive semitones. Boolean."
    return seq.open_scale

def closedscale(seq):
    "No room for non-consecutive semitones. Boolean."
    return seq.closed_scale

def primes(seq):
    "Show unique results in prime form. Boolean."
    return seq.is_prime()
     
def antiprimes(seq):
    "Show unique results in prime form. Boolean."
    return seq == seq.antiprime
    
def fortes (seq):
    "Show only Forte primes. Boolean."
    return seq.is_forte

def mirror(seq):
    "Sequences whose primes equal their prime inversions. Boolean."
    return seq.is_mirror

def follow(seq):
    "Influenced by leader instances. Boolean."
    if any(v is not None for v in seq.master_dict.values()):
        return all(getattr(seq, attr) == seq.master_dict[attr]
                for attr in seq.master_dict)

#List

def includes(seq, alist):
    "Step values to be included. Repeatable."
    return all(i in seq.prime_steps for i in alist)

def excludes(seq, alist):
    "Step values to be excluded. Repeatable."
    return not any(i in seq.prime_steps for i in alist)

def only(seq, alist):
    "Step values to be exclusively included. Repeatable."
    return set(seq.prime_steps) <= set(alist)

def allonly(seq, alist):
    "Step values to all be exclusively included. Repeatable."
    return set(seq.prime_steps) == set(alist)

##Pcset filters

def degrees(seq, alist):
    "Intervals to be included. Repeatable."
    return all(i in seq.pcset() for i in alist)

def nondegrees(seq, alist):
    "Intervals to be excluded. Repeatable."
    return not any(i in seq.pcset() for i in alist)

#Range

def length(seq, minmax):
    "Values or dash-separated ranges (x-y) for sequence length."   
    return minmax[0] <= len(seq) <= minmax[1]

def symmetry(seq, minmax):
    "Values or dash-separated ranges (x-y) for sequence symmetry."
    return minmax[0] <= seq.symmetry <= minmax[1]

def variety(seq, minmax):
    "Values or dash-separated ranges (x-y) for number of different step-values."
    return minmax[0] <= seq.variety <= minmax[1]

def zdepth(seq, minmax):
    """Values or dash-separated ranges (x-y) for number
    of different values in sequence's Z-vector."""
    return minmax[0] <= seq.z_depth <= minmax[1]

def zvariety(seq, minmax):
    """Values or dash-separated ranges (x-y) for number
    of non-zero values in sequence's Z-vector."""
    return minmax[0] <= seq.z_variety <= minmax[1]

def consonance(seq, minmax):
    "Values or dash-separated ranges (x-y) for sequence consonance."
    return minmax[0] <= seq.consonance <= minmax[1]

#Value and range

def fine(seq, alist):
    """Step values followed by single value or
    dash-separated range (x y-z) for number of occurences. Repeatable."""
    for num in range(len(alist) // 3):
        nums = [alist[3 * num]] + sorted(alist[3 * num + 1:3 * num + 3])
        if not nums[1] <= seq.prime_steps.count(nums[0]) <= nums[2]:
            return False
    return True

##Z-vector filters
def ztest(seq, alist):
    """Z-vector values, followed by single value or
    dash-separated range (x y-z) for number of occurences. Repeatable."""
    for num in range(len(alist) // 3):
        nums = [alist[3 * num]] + sorted(alist[3 * num + 1:3 * num + 3])
        if 0 < nums[0] <= len(seq.vector):
            if not nums[1] <= seq.vector[nums[0] - 1] <= nums[2]:
                return False
    return True


###Modifications:

#Boolean

def primify(seq):
    "Convert results to prime form. Boolean."
    if seq:
        seq.order = list(range(len(seq.prime)))
        seq.transpose(-min(seq))
            
def antiprimify(seq):
    "Convert results to antiprime form. Boolean."
    seq.__init__(*seq.antiprime, cardinal=seq.cardinal)   
    
def invert(seq):
    "Invert sequence. Boolean."
    seq.__init__(*seq.inverted(), cardinal=seq.cardinal)

def backwards(seq):
    "Reverse output. Boolean."
    seq.order.reverse()

@_reorder  
def svoice(seq):
    "Simple voicing algorithm. Boolean."
    seq.order = [i + len(seq.prime) * (1, 0, -1)[i % 3] for i in seq.order]
    
@_reorder
def compand(seq, value):
    "Put all values within given octave range."
    if seq and value:
        ran = range(abs(value))       
        seq.order = [j % len(seq.prime) + len(seq.prime) * ran[i % value]
        * abs(value) // value for i, j in enumerate(seq.order)]
             

def lead(seq):
    "Influences follower instances. Boolean."
    for name in seq.master_dict:
        seq.master_dict[name] = getattr(seq, name)

def rmultiply(seq):
    "Multiply sequence elements by a random integer (2-4). Boolean."
    seq *= _randint(2, 4)

##Modify via either pcset or steps

def rand(seq):
    "Randomise sequence order. Boolean."
    _shuffle(seq.order)

@_stepwrap
def srand(steps):
    "Randomise step order. Boolean."
    _shuffle(steps)    
      
def split(seq):
    "Alternating high and low values. Boolean."
    seq.order = [seq.order.pop(-(i % 2)) for i in range(len(seq))]
        
@_stepwrap
def ssplit(steps):
    "Alternating big and small steps. Boolean."
    steps[:] = [steps.pop(-(i % 2)) for i in range(len(steps))]
    
def sort(seq):
    "Sort sequence. Boolean."
    seq.sort()
    
@_stepwrap
def ssort(steps):
    "Sort steps. Boolean."
    steps.sort(key=abs)
                  
def updown(seq, value):
    "Notes go up then down n times."
    forward = value > 0
    value %= len(seq)
    if seq and value:
        piles = [[] for _ in range(value)]
        for i, j in enumerate(seq.order):
            piles[i % len(piles)].append(j)
        for i, j in enumerate(piles):
            if (i % 2 and forward) or (not i % 2 and not forward):
                j.reverse()
        seq.order = sum(piles, [])     
        
@_stepwrap    
def supdown(steps, value):
    "Steps go up and down n times."
    forward = value > 0
    value %= len(steps)
    if value:
        piles = [[] for _ in range(value)]
        for i, j in enumerate(steps):
            piles[i % len(piles)].append(j)
        for i, j in enumerate(piles):
            if (i % 2 and forward) or (not i % 2 and not forward):
                j.reverse()
        steps[:] = sum(piles, [])    

#Single value

def transpose(seq, value):
    "The number of semitones to transpose output."
    seq.transpose(value)

    
def strans(seq, value):
    "Transpose by steps"
    seq.transpose(sum(seq.steps()[:value]))
    
def spin(seq, value):
    "The number to transpose each sequence element within its range."
    old_order = seq.order
    seq.transpose(value)
    seq.order = old_order

def pcspin(seq, value):
    "The number of steps to rotate the underlying pitch-classes."
    seq.order = [(rem + value) % len(seq.prime) + octave * len(seq.prime)
                    for octave, rem in
                    (divmod(i, len(seq.prime)) for i in seq.order)]    

@_reorder       
def rvoice(seq, value):
    "The range to voice notes beyond the octave."
    if value and len(seq) > 1:
        limits = ((value, seq.cardinal) if value < 0 else
                (0, seq.cardinal + value))
        octs = value // seq.cardinal + ( -1 if value < 0 else 2)
        compand(seq, 1)
        for i, note in enumerate(seq):
            if (i % octs and
            limits[0] <= note + (seq.cardinal * (i % octs)) <=  limits[1]):
                seq.voice(i, i % octs)

        
@_reorder             
def ivoice(seq, value):
    "Voicing by minimum interval between notes."
    for i in range(len(seq) - 1):
        seq.sort(reverse=value < 0)
        for j in range(i + 1, len(seq)):
            while abs(seq[j] - seq[i]) < abs(value):
                seq.voice(j, abs(value)//value)
           

def multiply(seq, value):
    "Multiply sequence by an integer."
    seq *= value
    
def emultiply(seq, value):
    "Multiply sequence elements by an integer."
    seq.emult(value)

       
##Modify via either pcset or steps

def rotate(seq, value):
    "The number of elements to rotate each sequence."
    seq.order = seq.order[-value:] + seq.order[:-value]


def srotate(seq, value):
    "The number of steps to rotate each sequence."

    seq.order = ([i - len(seq.prime) for i in seq.order[-value:]] +
                seq.order[:-value])

def shuffle(seq, value):
    "A value determining the element shuffle offset."
    rev = value < 0
    value %= len(seq)
    if seq and value:
        piles = [[] for _ in range(value)]
        for i, j in enumerate(seq.order):
            piles[i % len(piles)].append(j)
        if rev:
            piles.reverse()    
        seq.order = sum(piles, [])    

@_stepwrap           
def sshuffle(steps, value):
    "A number determining the step shuffle offset."
    rev = value < 0    
    value %= len(steps)
    if value:
        piles = [[] for _ in range(value)]
        for i, j in enumerate(steps):
            piles[i % len(piles)].append(j)
        if rev:
            piles.reverse()            
        steps[:] = sum(piles, [])

#List
def reorder(seq, alist):
    """Space-separated sequence replacing the pitches;
    the order stays the same. Repeatable."""
    old_order = seq.order
    seq.__init__(*alist, cardinal=seq.cardinal)
    seq.order = old_order
    
def sreorder(seq, alist):
    """Space-separated sequence replacing the steps;
    the order stays the same. Repeatable."""
    if seq:
        base = min(seq)
        old_order = seq.order
        seq.__init__(*alist, cardinal=seq.cardinal, steps=True)
        seq.order = old_order
        seq.transpose(base - min(seq))

##Modify via either pcset or steps

def pattern(seq, alist):
    """Space-separated sequence index numbers
    determining the order of elements. Repeatable"""
    if seq:
        seq.order = [seq.order[i % len(seq)] for i in alist]
        
@_stepwrap
def spattern(steps, alist):
    """Space-separated sequence index numbers
    determining the order of steps. Repeatable"""
        
    steps[:] = [steps[i % len(steps)] for i in alist]

