import numpy as np

from joatmon.ai.core import CoreRandom


# another class to make the noise decayed, parent class for all
class GaussianRandom(CoreRandom):
    """
    Gaussian Noise

    # Arguments
        mu (float): .
        size (int): .
        sigma (float): .
        sigma_min (float): .
        decay_steps (int): .
    """

    def __init__(self, mu=0.0, size=2, sigma=0.1, sigma_min=0.01, decay_steps=200000):
        super().__init__()

        self.mu = mu
        self.sigma = sigma
        self.sigma_min = sigma_min
        self.size = size

        self.m = -float(sigma - sigma_min) / float(decay_steps)
        self.c = sigma

        self.n_steps = 0
        self.current_sigma = max(self.sigma_min, self.m * float(self.n_steps) + self.c)
        self.reset()

    def reset(self):
        self.n_steps = 0
        self.current_sigma = max(self.sigma_min, self.m * float(self.n_steps) + self.c)

    def decay(self):
        self.n_steps += 1
        self.current_sigma = max(self.sigma_min, self.m * float(self.n_steps) + self.c)

    def sample(self):
        x = np.random.normal(loc=self.mu, scale=self.current_sigma, size=self.size)
        return x


class OrnsteinUhlenbeck(CoreRandom):
    """
    Ornstein Uhlenbeck Process

    # Arguments
        dt (float): .
        mu (float): .
        size (int): .
        sigma (float): .
        theta (float): .
        sigma_min (float): .
        decay_steps (int): .
    """

    def __init__(self, dt=1.0, mu=0.0, size=2, sigma=0.1, theta=0.15, sigma_min=0.01, decay_steps=200000):
        super().__init__()

        self.dt = dt
        self.mu = mu
        self.size = size
        self.sigma = sigma
        self.theta = theta
        self.sigma_min = sigma_min

        self.m = -float(sigma - sigma_min) / float(decay_steps)
        self.c = sigma

        self.n_steps = 0
        self.x_prev = np.ones(self.size) * self.mu
        self.current_sigma = max(self.sigma_min, self.m * float(self.n_steps) + self.c)
        self.reset()

    def reset(self):
        self.n_steps = 0
        self.x_prev = np.ones(self.size) * self.mu
        self.current_sigma = max(self.sigma_min, self.m * float(self.n_steps) + self.c)

    def decay(self):
        self.n_steps += 1
        self.current_sigma = max(self.sigma_min, self.m * float(self.n_steps) + self.c)

    def sample(self):
        x = self.x_prev + self.theta * (self.mu - self.x_prev) * self.dt + self.current_sigma * np.sqrt(self.dt) * np.random.normal(size=self.size)
        self.x_prev = x
        return x
