"""
Facilities to generate synthetic stochastic processes
"""

import numpy as np


class SignalEmulator(object):
    """
    Emulate a complex signal built from several additive and non-additive components
    """

    def __init__(self, **kwargs):
        super(SignalEmulator, self).__init__()

        self.components = []
        """Components of the signal"""

    def stationary_gaussian(self, mu:float, sigma:float, **kwargs):
        """
        Creates a continuous Gaussian signal with mean mu and variance sigma.

        :param mu: mean
        :param sigma: variance
        :keyword additive: If False it cancels the previous signal and start this one, if True
                           this signal is added to the previous one
        :keyword start: lag index to start this signal, the default value is 0
        :keyword it: Number of iterations, the default value is 1
        :keyword length: Number of samples generated on each iteration, the default value is 100
        :keyword vmin: Lower bound value of generated data, the default value is None
        :keyword vmax: Upper bound value of generated data, the default value is None
        :return: the current SignalEmulator instance, for method chaining
        """
        parameters = {'mu': mu, 'sigma': sigma}
        self.components.append({'dist': 'gaussian', 'type': 'constant',
                                'parameters': parameters, 'args': kwargs})
        return self

    def incremental_gaussian(self, mu:float, sigma:float, **kwargs):
        """
        Creates an additive gaussian interference on a previous signal

        :param mu: increment on mean
        :param sigma: increment on variance
        :keyword start: lag index to start this signal, the default value is 0
        :keyword it: Number of iterations, the default value is 1
        :keyword length: Number of samples generated on each iteration, the default value is 100
        :keyword vmin: Lower bound value of generated data, the default value is None
        :keyword vmax: Upper bound value of generated data, the default value is None
        :return: the current SignalEmulator instance, for method chaining
        """
        parameters = {'mu': mu, 'sigma': sigma}
        self.components.append({'dist': 'gaussian', 'type': 'incremental',
                                'parameters': parameters, 'args': kwargs})
        return self

    def periodic_gaussian(self, type:str, period:int, mu_min:float, sigma_min:float, mu_max:float, sigma_max:float, **kwargs):
        """
        Creates an additive periodic gaussian interference on a previous signal

        :param type: 'linear' or 'sinoidal'
        :param period: the period of recurrence
        :param mu: increment on mean
        :param sigma: increment on variance
        :keyword start: lag index to start this signal, the default value is 0
        :keyword it: Number of iterations, the default value is 1
        :keyword length: Number of samples generated on each iteration, the default value is 100
        :keyword vmin: Lower bound value of generated data, the default value is None
        :keyword vmax: Upper bound value of generated data, the default value is None
        :return: the current SignalEmulator instance, for method chaining
        """
        parameters = {'type':type, 'period':period,
                      'mu_min': mu_min, 'sigma_min': sigma_min, 'mu_max': mu_max, 'sigma_max': sigma_max}
        self.components.append({'dist': 'gaussian', 'type': 'periodic',
                                'parameters': parameters, 'args': kwargs})
        return self

    def blip(self, **kwargs):
        """
        Creates an outlier greater than the maximum or lower then the minimum previous values of the signal,
        and insert it on a random location of the signal.

        :return: the current SignalEmulator instance, for method chaining
        """
        parameters = {}
        self.components.append({'dist': 'blip', 'type': 'blip',
                                'parameters': parameters, 'args':kwargs})
        return self

    def run(self):
        """
        Render the signal

        :return: a list of float values
        """
        signal = []
        last_it = 10
        last_num = 10
        for ct, component in enumerate(self.components):
            parameters = component['parameters']
            kwargs = component['args']
            additive = kwargs.get('additive', True)
            start = kwargs.get('start', 0)
            it = kwargs.get('it', last_it)
            num = kwargs.get('length', last_num)
            vmin = kwargs.get('vmin',None)
            vmax = kwargs.get('vmax', None)
            if component['type'] == 'constant':
                tmp = generate_gaussian_linear(parameters['mu'], parameters['sigma'], 0, 0,
                                         it=it, num=num, vmin=vmin, vmax=vmax)
            elif component['type'] == 'incremental':
                tmp = generate_gaussian_linear(0, 0, parameters['mu'], parameters['sigma'],
                                         it=num, num=1, vmin=vmin, vmax=vmax)
            elif component['type'] == 'periodic':
                period = parameters['period']
                mu_min, sigma_min = parameters['mu_min'],parameters['sigma_min']
                mu_max, sigma_max = parameters['mu_max'],parameters['sigma_max']

                if parameters['type'] == 'sinoidal':
                    tmp = generate_sinoidal_periodic_gaussian(period, mu_min, sigma_min, mu_max, sigma_max,
                                                              it=num, num=1, vmin=vmin, vmax=vmax)
                else:
                    tmp = generate_linear_periodic_gaussian(period, mu_min, sigma_min, mu_max, sigma_max,
                                                        it=num, num=1, vmin=vmin, vmax=vmax)
            elif component['type'] == 'blip':
                _mx = np.nanmax(signal)
                _mn = np.nanmin(signal)

                _mx += 2*_mx if _mx > 0 else -2*_mx
                _mn += -2*_mn if _mn > 0 else 2*_mn

                if vmax is not None:
                    _mx = min(_mx, vmax) if vmax > 0 else max(_mx, vmax)
                if vmin is not None:
                    _mn = max(_mn, vmin) if vmin > 0 else min(_mn, vmin)

                start = np.random.randint(0, len(signal))
                tmp = [_mx] if np.random.rand() >= .5 else [-_mn]

            last_num = num
            last_it = it

            signal = _append(additive, start, signal, tmp)

        return signal




def generate_gaussian_linear(mu_ini, sigma_ini, mu_inc, sigma_inc, it=100, num=10, vmin=None, vmax=None):
    """
    Generate data sampled from Gaussian distribution, with constant or linear changing parameters

    :param mu_ini: Initial mean
    :param sigma_ini: Initial variance
    :param mu_inc:  Mean increment after 'num' samples
    :param sigma_inc: Variance increment after 'num' samples
    :param it: Number of iterations
    :param num: Number of samples generated on each iteration
    :param vmin: Lower bound value of generated data
    :param vmax: Upper bound value of generated data
    :return: A list of it*num float values
    """
    mu = mu_ini
    sigma = sigma_ini
    ret = []
    for k in np.arange(0,it):
        tmp = np.random.normal(mu, sigma, num)
        if vmin is not None:
            tmp = np.maximum(np.full(num, vmin), tmp)
        if vmax is not None:
            tmp = np.minimum(np.full(num, vmax), tmp)
        ret.extend(tmp)
        mu += mu_inc
        sigma += sigma_inc
    return ret


def generate_linear_periodic_gaussian(period, mu_min, sigma_min, mu_max, sigma_max, it=100, num=10, vmin=None, vmax=None):
    """
    Generates a periodic linear variation on mean and variance

    :param period: the period of recurrence
    :param mu_min: initial (and minimum) mean of each period
    :param sigma_min: initial (and minimum) variance of each period
    :param mu_max: final (and maximum) mean of each period
    :param sigma_max: final (and maximum) variance of each period
    :param it: Number of iterations
    :param num: Number of samples generated on each iteration
    :param vmin: Lower bound value of generated data
    :param vmax: Upper bound value of generated data
    :return: A list of it*num float values
    """

    if period > it:
        raise("The 'period' parameter must be lesser than 'it' parameter")

    mu_inc = (mu_max - mu_min)/period
    sigma_inc = (sigma_max - sigma_min) / period
    mu = mu_min
    sigma = sigma_min
    ret = []
    signal = True

    for k in np.arange(0, it):
        tmp = np.random.normal(mu, sigma, num)
        if vmin is not None:
            tmp = np.maximum(np.full(num, vmin), tmp)
        if vmax is not None:
            tmp = np.minimum(np.full(num, vmax), tmp)
        ret.extend(tmp)

        if k % period == 0:
            signal = not signal

        mu += (mu_inc if signal else -mu_inc)
        sigma += (sigma_inc if signal else -sigma_inc)

        sigma = max(sigma, 0.005)

    return ret


def generate_sinoidal_periodic_gaussian(period, mu_min, sigma_min, mu_max, sigma_max, it=100, num=10, vmin=None, vmax=None):
    """
    Generates a periodic sinoidal variation on mean and variance

    :param period: the period of recurrence
    :param mu_min: initial (and minimum) mean of each period
    :param sigma_min: initial (and minimum) variance of each period
    :param mu_max: final (and maximum) mean of each period
    :param sigma_max: final (and maximum) variance of each period
    :param it: Number of iterations
    :param num: Number of samples generated on each iteration
    :param vmin: Lower bound value of generated data
    :param vmax: Upper bound value of generated data
    :return: A list of it*num float values
    """
    mu_range = mu_max - mu_min
    sigma_range = sigma_max - sigma_min
    mu = mu_min
    sigma = sigma_min
    ret = []

    for k in np.arange(0, it):
        tmp = np.random.normal(mu, sigma, num)
        if vmin is not None:
            tmp = np.maximum(np.full(num, vmin), tmp)
        if vmax is not None:
            tmp = np.minimum(np.full(num, vmax), tmp)
        ret.extend(tmp)

        mu += mu_range * np.sin(period * k)
        sigma += sigma_range * np.sin(period * k)

        sigma = max(sigma, 0.005)

    return ret


def generate_uniform_linear(min_ini, max_ini, min_inc, max_inc, it=100, num=10, vmin=None, vmax=None):
    """
    Generate data sampled from Uniform distribution, with constant or  linear changing bounds

    :param mu_ini: Initial mean
    :param sigma_ini: Initial variance
    :param mu_inc:  Mean increment after 'num' samples
    :param sigma_inc: Variance increment after 'num' samples
    :param it: Number of iterations
    :param num: Number of samples generated on each iteration
    :param vmin: Lower bound value of generated data
    :param vmax: Upper bound value of generated data
    :return: A list of it*num float values
    """
    _min = min_ini
    _max = max_ini
    ret = []
    for k in np.arange(0,it):
        tmp = np.random.uniform(_min, _max, num)
        if vmin is not None:
            tmp = np.maximum(np.full(num, vmin), tmp)
        if vmax is not None:
            tmp = np.minimum(np.full(num, vmax), tmp)
        ret.extend(tmp)
        _min += min_inc
        _max += max_inc
    return ret


def white_noise(n=500):
    """
    Simple Gaussian noise signal
    :param n: number of samples
    :return:
    """
    return np.random.normal(0, 1, n)


def random_walk(n=500, type='gaussian'):
    """
    Simple random walk

    :param n: number of samples
    :param type: 'gaussian' or 'uniform'
    :return:
    """
    if type == 'gaussian':
        tmp = generate_gaussian_linear(0, 1, 0, 0, it=1, num=n)
    else:
        tmp = generate_uniform_linear(-1, 1, 0, 0, it=1, num=n)
    ret = [0]
    for i in range(n):
        ret.append(tmp[i] + ret[i])

    return ret


def _append(additive, start, before, new):
    if not additive:
        before.extend(new)
        return before
    else:
        for k in range(start):
            new.insert(0,0)

        l1 = len(before)
        l2 = len(new)

        if l2 < l1:
            new.extend(np.zeros(l1 - l2).tolist())
        elif 0 < l1 < l2:
            new = new[:l1]

        if len(before) == 0:
            tmp = np.array(new)
        else:
            tmp = np.array(before) + np.array(new)
        return tmp.tolist()


