'''
Created on Sep 18, 2013

@author: timmahrt

Given two lists of tuples of the form [(value, time), (value, time)], morph
can iteratively transform the values in one list to the values in the other
while maintaining the times in the first list.

Both time scales are placed on a relative scale.  This assumes that the times
may be different and the number of samples may be different but the 'events'
occur at the same relative location (half way through, at the end, etc.).

Both dynamic time warping and morph, align two data lists in time.  However,
dynamic time warping does this by analyzing the event structure and aligning
events in the two signals as best it can
(i.e. it changes when events happen in relative time while morph preserves
when events happen in relative time).
'''


class RelativizeSequenceException(Exception):

    def __init__(self, dist):
        super(RelativizeSequenceException, self).__init__()
        self.dist = dist

    def __str__(self):
        return "You need at least two unique values to make " + \
            "a sequence relative. Input: %s" % repr(self.dist)


def makeSequenceRelative(absVSequence):
    '''
    Puts every value in a list on a continuum between 0 and 1

    Also returns the min and max values (to reverse the process)
    '''

    if len(absVSequence) < 2 or len(set(absVSequence)) == 1:
        raise RelativizeSequenceException(absVSequence)

    minV = min(absVSequence)
    maxV = max(absVSequence)
    relativeSeq = [(value - minV) / (maxV - minV) for value in absVSequence]

    return relativeSeq, minV, maxV


def makeSequenceAbsolute(relVSequence, minV, maxV):
    '''
    Makes every value in a sequence absolute
    '''

    return [(value * (maxV - minV)) + minV for value in relVSequence]


def _makeTimingRelative(absoluteDataList):
    '''
    Given normal pitch tier data, puts the times on a scale from 0 to 1

    Input is a list of tuples of the form
    ([(time1, pitch1), (time2, pitch2),...]

    Also returns the start and end time so that the process can be reversed
    '''

    timingSeq = [row[0] for row in absoluteDataList]
    valueSeq = [list(row[1:]) for row in absoluteDataList]

    relTimingSeq, startTime, endTime = makeSequenceRelative(timingSeq)
    
    relDataList = [tuple([time, ] + row) for time, row
                   in zip(relTimingSeq, valueSeq)]

    return relDataList, startTime, endTime


def _makeTimingAbsolute(relativeDataList, startTime, endTime):
    '''
    Maps values from 0 to 1 to the provided start and end time

    Input is a list of tuples of the form
    ([(time1, pitch1), (time2, pitch2),...]
    '''

    timingSeq = [row[0] for row in relativeDataList]
    valueSeq = [list(row[1:]) for row in relativeDataList]
    
    absTimingSeq = makeSequenceAbsolute(timingSeq, startTime, endTime)

    absDataList = [tuple([time, ] + row) for time, row
                   in zip(absTimingSeq, valueSeq)]

    return absDataList


def _getSmallestDifference(inputList, targetVal):
    '''
    Returns the value in inputList that is closest to targetVal
    
    Iteratively splits the dataset in two, so it should be pretty fast
    '''
    targetList = inputList[:]
    retVal = None
    while True:
        # If we're down to one value, stop iterating
        if len(targetList) == 1:
            retVal = targetList[0]
            break
        halfPoint = int(len(targetList) / 2.0) - 1
        a = targetList[halfPoint]
        b = targetList[halfPoint + 1]
        
        leftDiff = abs(targetVal - a)
        rightDiff = abs(targetVal - b)
        
        # If the distance is 0, stop iterating, the targetVal is present
        # in the inputList
        if leftDiff == 0 or rightDiff == 0:
            retVal = targetVal
            break
        
        # Look at left half or right half
        if leftDiff < rightDiff:
            targetList = targetList[:halfPoint + 1]
        else:
            targetList = targetList[halfPoint + 1:]
         
    return retVal
        
    
def _getNearestMappingIndexList(fromValList, toValList):
    '''
    Finds the indicies for data points that are closest to each other.

    The inputs should be in relative time, scaled from 0 to 1
    e.g. if you have [0, .1, .5., .9] and [0, .1, .2, 1]
    will output [0, 1, 1, 2]
    '''

    indexList = []
    for fromTimestamp in fromValList:
        smallestDiff = _getSmallestDifference(toValList, fromTimestamp)
        i = toValList.index(smallestDiff)
        indexList.append(i)

    return indexList


def morphDataLists(fromList, toList, stepList):
    '''
    Iteratively morph fromList into toList using the values 0 to 1 in stepList
    
    stepList: a value of 0 means no change and a value of 1 means a complete
    change to the other value
    '''

    # If there are more than 1 pitch value, then we align the data in
    # relative time.
    # Each data point comes with a timestamp.  The earliest timestamp is 0
    # and the latest timestamp is 1.  Using this method, for each relative
    # timestamp in the source list, we find the closest relative timestamp
    # in the target list.  Just because two pitch values have the same index
    # in the source and target lists does not mean that they correspond to
    # the same speech event.
    fromListRel, fromStartTime, fromEndTime = _makeTimingRelative(fromList)
    toListRel = _makeTimingRelative(toList)[0]

    # If fromList has more points, we'll have flat areas
    # If toList has more points, we'll might miss peaks or valleys
    fromTimeList = [dataTuple[0] for dataTuple in fromListRel]
    toTimeList = [dataTuple[0] for dataTuple in toListRel]
    indexList = _getNearestMappingIndexList(fromTimeList, toTimeList)
    alignedToPitchRel = [toListRel[i] for i in indexList]

    for stepAmount in stepList:
        newPitchList = []

        # Perform the interpolation
        for fromTuple, toTuple in zip(fromListRel, alignedToPitchRel):
            fromTime, fromValue = fromTuple
            toTime, toValue = toTuple

            # i + 1 b/c i_0 = 0 = no change
            newValue = fromValue + (stepAmount * (toValue - fromValue))
            newTime = fromTime + (stepAmount * (toTime - fromTime))

            newPitchList.append((newTime, newValue))

        newPitchList = _makeTimingAbsolute(newPitchList, fromStartTime,
                                           fromEndTime)

        yield stepAmount, newPitchList


def morphChunkedDataLists(fromDataList, toDataList, stepList):
    '''
    Morph one set of data into another, in a stepwise fashion

    A convenience function.  Given a set of paired data lists,
    this will morph each one individually.

    Returns a single list with all data combined together.
    '''

    assert(len(fromDataList) == len(toDataList))

    # Morph the fromDataList into the toDataList
    outputList = []
    for x, y in zip(fromDataList, toDataList):

        # We cannot morph a region if there is no data or only
        # a single data point for either side
        if (len(x) < 2) or (len(y) < 2):
            continue

        tmpList = [outputPitchList for _, outputPitchList
                   in morphDataLists(x, y, stepList)]
        outputList.append(tmpList)

    # Transpose list
    finalOutputList = outputList.pop(0)
    for subList in outputList:
        for i, subsubList in enumerate(subList):
            finalOutputList[i].extend(subsubList)

    return finalOutputList


def morphAveragePitch(fromDataList, toDataList):
    '''
    Adjusts the values in fromPitchList to have the same average as toPitchList
    
    Because other manipulations can alter the average pitch, morphing the pitch
    is the last pitch manipulation that should be done
    
    After the morphing, the code removes any values below zero, thus the
    final average might not match the target average.
    '''
    
    timeList, fromPitchList = zip(*fromDataList)
    toPitchList = [pitchVal for _, pitchVal in toDataList]
    
    # Zero pitch values aren't meaningful, so filter them out if they are
    # in the dataset
    fromListNoZeroes = [val for val in fromPitchList if val > 0]
    fromAverage = sum(fromListNoZeroes) / float(len(fromListNoZeroes))
    
    toListNoZeroes = [val for val in toPitchList if val > 0]
    toAverage = sum(toListNoZeroes) / float(len(toListNoZeroes))
    
    newPitchList = [val - fromAverage + toAverage for val in fromPitchList]