# -*- coding: utf-8 -*-
"""
@author: Laura C. Kühle

"""
from abc import ABC, abstractmethod
import numpy as np


class InitialCondition(ABC):
    """Abstract class for initial condition function.

    Methods
    -------
    get_name()
        Returns string of class name.
    is_smooth()
        Returns flag whether function is smooth.
    induce_adjustment(value)
        Adjusts x-value of function.
    randomize(config)
        Sets all non-determined instance variables to random value.
    calculate(x)
        Evaluates function at given x-value.

    """
    def __init__(self, config):
        """Initializes InitialCondition.

        Parameters
        ----------
        config : dict
            Additional parameters for initial condition.

        """
        self._reset(config)

    def _reset(self, config):
        """Resets instance variables.

        Parameters
        ----------
        config : dict
            Additional parameters for initial condition.

        """
        pass

    def get_name(self):
        """Returns string of class name."""
        return self.__class__.__name__

    def is_smooth(self):
        """Returns flag that function is smooth."""
        return True

    def induce_adjustment(self, value):
        """Adjusts x-value of function.

        Parameters
        ----------
        value : float
            Value of adjustment on x-axis in right direction.

        """
        pass

    def randomize(self, config):
        """Sets all non-determined instance variables to random value.

        Parameters
        ----------
        config : dict
            Fixed parameters for initial condition.

        """
        pass

    def calculate(self, x, mesh=None):
        """Evaluates function at given x-value.

        If a mesh is given, the x-value is projected into its periodic
        interval before evaluating the function.

        Parameters
        ----------
        x : float
            Evaluation point of function.
        mesh : Mesh, optional
            Mesh for calculation. Default: None.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        if mesh is not None:
            left_bound, right_bound = mesh.bounds
            while x < left_bound:
                x += mesh.interval_len
            while x > right_bound:
                x -= mesh.interval_len
        return self._get_point(x)

    @abstractmethod
    def _get_point(self, x):
        """Evaluates function at given x-value.

        Parameters
        ----------
        x : float
            Evaluation point of function.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        pass


class Sine(InitialCondition):
    """Class for the sine wave.

    Attributes
    ----------
    factor : float
        Factor by which is the evaluation point is multiplied before applying
        sine.

    Methods
    -------
    randomize(config)
        Sets all non-determined instance variables to random value.

    """
    def _reset(self, config):
        """Resets instance variables.

        Parameters
        ----------
        config : dict
            Additional parameters for initial condition.

        """
        super()._reset(config)

        # Unpack necessary configurations
        self._factor = config.pop('factor', 2)

    def randomize(self, config):
        """Sets all non-determined instance variables to random value.

        Parameters
        ----------
        config : dict
            Fixed parameters for initial condition.

        """
        factor = config.pop('factor', np.random.uniform(low=-100, high=100))
        config = {'factor': factor}
        self._reset(config)

    def _get_point(self, x):
        """Evaluates function at given x-value.

        Parameters
        ----------
        x : float
            Evaluation point of function.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        return np.sin(self._factor * np.pi * x)


class Box(InitialCondition):
    """Class for a continuous function with two jumps (forming a box).

    Methods
    -------
    is_smooth()
        Returns flag whether function is smooth.

    """
    def is_smooth(self):
        """Returns flag that function is not smooth."""
        return False

    def _get_point(self, x):
        """Evaluates function at given x-value.

        Parameters
        ----------
        x : float
            Evaluation point of function.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        if x < -1:
            x += 2
        if x > 1:
            x -= 2
        if (x >= -0.5) & (x <= 0.5):
            return 1
        else:
            return 0


class FourPeakWave(InitialCondition):
    """Class for a function with four peaks.

    The function is defined piece-by-piece and consists of a Gaussian function,
    a box function, a symmetric absolute function, and an elliptic function.

    Attributes
    ----------
    alpha : float
        Factor used for the elliptic function.
    delta : float
        Value used to adjust z for the Gaussian function.
    beta : float
        Factor used for the Gaussian function.
    a : float
        Value for x-value adjustment for elliptic function.
    z : float
        Value for x-value adjustment for Gaussian function.

    Methods
    -------
    is_smooth()
        Returns flag whether function is smooth.

    """
    def _reset(self, config):
        """Resets instance variables.

        Parameters
        ----------
        config : dict
            Additional parameters for initial condition.

        """
        super()._reset(config)

        # Set additional necessary parameter
        self._alpha = 10
        self._delta = 0.005
        self._beta = np.log(2) / (36 * self._delta**2)
        self._a = 0.5
        self._z = -0.7

    def is_smooth(self):
        """Returns flag that function is not smooth."""
        return False

    def _get_point(self, x):
        """Evaluates function at given x-value.

        Parameters
        ----------
        x : float
            Evaluation point of function.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        if (x >= -0.8) & (x <= -0.6):
            return 1/6 * (self._gaussian_function(x, self._z-self._delta)
                          + self._gaussian_function(x, self._z+self._delta)
                          + 4 * self._gaussian_function(x, self._z))
        if (x >= -0.4) & (x <= -0.2):
            return 1
        if (x >= 0) & (x <= 0.2):
            return 1 - abs(10 * (x-0.1))
        if (x >= 0.4) & (x <= 0.6):
            return 1/6 * (self._elliptic_function(x, self._a-self._delta)
                          + self._elliptic_function(x, self._a+self._delta)
                          + 4 * self._elliptic_function(x, self._a))
        return 0

    def _gaussian_function(self, x, z):
        """Evaluates Gaussian function.

        Parameters
        ----------
        x : float
            Evaluation point of function.
        z : float
            Value for x-value adjustment.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        return np.exp(-self._beta * (x-z)**2)

    def _elliptic_function(self, x, a):
        """Evaluates elliptic function.

        Parameters
        ----------
        x : float
            Evaluation point of function.
        a : float
            Value for x-value adjustment.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        return np.sqrt(max(1 - self._alpha**2 * (x-a)**2, 0))


class Linear(InitialCondition):
    """Class for the linear function.

    Attributes
    ----------
    factor : float
        Factor by which is the evaluation point is multiplied.

    Methods
    -------
    randomize(config)
        Sets all non-determined instance variables to random value.

    """
    def _reset(self, config):
        """Resets instance variables.

        Parameters
        ----------
        config : dict
            Additional parameters for initial condition.

        """
        super()._reset(config)

        # Unpack necessary configurations
        self._factor = config.pop('factor', 1)

    def randomize(self, config):
        """Sets all non-determined instance variables to random value.

        Parameters
        ----------
        config : dict
            Fixed parameters for initial condition.

        """
        factor = config.pop('factor', np.random.uniform(low=-100, high=100))
        config = {'factor': factor}
        self._reset(config)

    def _get_point(self, x):
        """Evaluates function at given x-value.

        Parameters
        ----------
        x : float
            Evaluation point of function.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        return self._factor * x


class LinearAbsolut(InitialCondition):
    """Class for the absolute values of the linear function.

    Attributes
    ----------
    factor : float
        Factor by which is the evaluation point is multiplied.

    Methods
    -------
    is_smooth()
        Returns flag whether function is smooth.
    randomize(config)
        Sets all non-determined instance variables to random value.

    """
    def _reset(self, config):
        """Resets instance variables.

        Parameters
        ----------
        config : dict
            Additional parameters for initial condition.

        """
        super()._reset(config)

        # Unpack necessary configurations
        self._factor = config.pop('factor', 1)

    def is_smooth(self):
        """Returns flag that function is not smooth."""
        return False

    def randomize(self, config):
        """Sets all non-determined instance variables to random value.

        Parameters
        ----------
        config : dict
            Fixed parameters for initial condition.

        """
        factor = config.pop('factor', np.random.uniform(low=-100, high=100))
        config = {'factor': factor}
        self._reset(config)

    def _get_point(self, x):
        """Evaluates function at given x-value.

        Parameters
        ----------
        x : float
            Evaluation point of function.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        return self._factor * abs(x)


class DiscontinuousConstant(InitialCondition):
    """Class for the otherwise continuous function with one jump.

    Attributes
    ----------
    x0 : float
        X-value where jump is induced.
    left_factor : float
        Factor by which is the evaluation point is multiplied before the jump.
    right_factor : float
        Factor by which is the evaluation point is multiplied after the jump.

    Methods
    -------
    is_smooth()
        Returns flag whether function is smooth.

    """
    def _reset(self, config):
        """Resets instance variables.

        Parameters
        ----------
        config : dict
            Additional parameters for initial condition.

        """
        super()._reset(config)

        # Unpack necessary configurations
        self._x0 = config.pop('x0', 0)
        self._left_factor = config.pop('left_factor', 1)
        self._right_factor = config.pop('right_factor', 0.5)

    def is_smooth(self):
        """Returns flag that function is not smooth."""
        return False

    def _get_point(self, x):
        """Evaluates function at given x-value.

        Parameters
        ----------
        x : float
            Evaluation point of function.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        return self._left_factor * (x <= self._x0) \
            + self._right_factor * (x > self._x0)


class Polynomial(InitialCondition):
    """Class for the polynomial function.

    Attributes
    ----------
    factor : float
        Factor by which is the evaluation point is multiplied.
    exponential : int
        Degree of the polynomial.

    Methods
    -------
    randomize(config)
        Sets all non-determined instance variables to random value.

    """
    def _reset(self, config):
        """Resets instance variables.

        Parameters
        ----------
        config : dict
            Additional parameters for initial condition.

        """
        super()._reset(config)

        # Unpack necessary configurations
        self._factor = config.pop('factor', 1)
        self._exponential = config.pop('exponential', 2)

    def randomize(self, config):
        """Sets all non-determined instance variables to random value.

        Parameters
        ----------
        config : dict
            Fixed parameters for initial condition.

        """
        factor = config.pop('factor', np.random.uniform(low=-100, high=100))
        exponential = config.pop('exponential', np.random.randint(2, high=6))
        config = {'factor': factor, 'exponential': exponential}
        self._reset(config)

    def _get_point(self, x):
        """Evaluates function at given x-value.

        Parameters
        ----------
        x : float
            Evaluation point of function.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        return self._factor * (x ** self._exponential)


class Continuous(InitialCondition):
    """Class for the continuous function.

    Attributes
    ----------
    factor : float
        Factor by which is the evaluation point is multiplied.

    Methods
    -------
    randomize(config)
        Sets all non-determined instance variables to random value.

    """
    def _reset(self, config):
        """Resets instance variables.

        Parameters
        ----------
        config : dict
            Additional parameters for initial condition.

        """
        super()._reset(config)

        # Unpack necessary configurations
        self._factor = config.pop('factor', 1)

    def randomize(self, config):
        """Sets all non-determined instance variables to random value.

        Parameters
        ----------
        config : dict
            Fixed parameters for initial condition.

        """
        factor = config.pop('factor', np.random.uniform(low=-100, high=100))
        config = {'factor': factor}
        self._reset(config)

    def _get_point(self, x):
        """Evaluates function at given x-value.

        Parameters
        ----------
        x : float
            Evaluation point of function.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        return self._factor


class HeavisideOneSided(InitialCondition):
    """Class for the one-sided heaviside function.

    Attributes
    ----------
    factor : float
        Factor by which is the evaluation point is multiplied.

    Methods
    -------
    is_smooth()
        Returns flag whether function is smooth.
    randomize(config)
        Sets all non-determined instance variables to random value.

    """
    def _reset(self, config):
        """Resets instance variables.

        Parameters
        ----------
        config : dict
            Additional parameters for initial condition.

        """
        super()._reset(config)

        # Unpack necessary configurations
        self._factor = config.pop('factor', -1)

    def is_smooth(self):
        """Returns flag that function is not smooth."""
        return False

    def randomize(self, config):
        """Sets all non-determined instance variables to random value.

        Parameters
        ----------
        config : dict
            Fixed parameters for initial condition.

        """
        factor = config.pop('factor', np.random.choice([-1, 1]))
        config = {'factor': factor}
        self._reset(config)

    def _get_point(self, x):
        """Evaluates function at given x-value.

        Parameters
        ----------
        x : float
            Evaluation point of function.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        return self._factor - 2 * self._factor * np.heaviside(x, 0)


class HeavisideTwoSided(InitialCondition):
    """Class for the two-sided heaviside function.

    Attributes
    ----------
    left_factor : float
        Factor by which is the evaluation point is multiplied before the jump.
    right_factor : float
        Factor by which is the evaluation point is multiplied after the jump.
    adjustment : float
        Extent of adjustment of evaluation point in x-direction.

    Methods
    -------
    is_smooth()
        Returns flag whether function is smooth.
    induce_adjustment(value)
        Adjusts x-value of function.
    randomize(config)
        Sets all non-determined instance variables to random value.

    """
    def _reset(self, config):
        """Resets instance variables.

        Parameters
        ----------
        config : dict
            Additional parameters for initial condition.

        """
        super()._reset(config)

        # Unpack necessary configurations
        self._left_factor = config.pop('left_factor', 1)
        self._right_factor = config.pop('right_factor', 2)
        self._adjustment = config.pop('adjustment', 0)

    def is_smooth(self):
        """Returns flag that function is not smooth."""
        return False

    def induce_adjustment(self, value):
        """Adjusts x-value of function.

        Parameters
        ----------
        value : float
            Value of adjustment on x-axis in right direction.

        """
        self._adjustment = value

    def randomize(self, config):
        """Sets all non-determined instance variables to random value.

        Parameters
        ----------
        config : dict
            Fixed parameters for initial condition.

        """
        left_factor = config.pop('left_factor', np.random.choice([-1, 1]))
        right_factor = config.pop('right_factor', np.random.choice([-1, 1]))
        adjustment = config.pop('adjustment',
                                np.random.uniform(low=-1, high=1))
        config = {'left_factor': left_factor, 'right_factor': right_factor,
                  'adjustment': adjustment}
        self._reset(config)

    def _get_point(self, x):
        """Evaluates function at given x-value.

        Parameters
        ----------
        x : float
            Evaluation point of function.

        Returns
        -------
        float
            Value of function evaluates at x-value.

        """
        return self._left_factor \
            - self._left_factor * np.heaviside(x - self._adjustment, 0)\
            - self._right_factor * np.heaviside(x + self._adjustment, 0)