"""The :mod:`variation` module defines classes for variation operators.

Variation operators (aka genetic operators) are used in evolutionary/genetic
algorithms to create "child" genomes from "parent" genomes.

"""
from abc import ABC, abstractmethod
from typing import Sequence, Union
import math

from numpy.random import random, choice

from pyshgp.push.types import PushType
from pyshgp.push.atoms import Literal
from pyshgp.gp.genome import Genome, GeneSpawner
from pyshgp.tap import tap
from pyshgp.utils import DiscreteProbDistrib, instantiate_using


class VariationOperator(ABC):
    """Base class of all VariationOperators.

    Parameters
    ----------
    num_parents : int
        Number of parent Genomes the operator needs to produce a child
        Individual.

    Attributes
    ----------
    num_parents : int
        Number of parent Genomes the operator needs to produce a child
        Individual.

    """

    def __init__(self, num_parents: int):
        self.num_parents = num_parents

    def checknum_parents(self, parents: Sequence[Genome]):
        """Raise error if given too few parents.

        Parameters
        ----------
        parents
            A list of parent Genomes given to the operator.

        """
        if not len(parents) >= self.num_parents:
            raise ValueError("Variation operator given {a} parents. Expected {e}.".format(
                a=len(parents),
                e=self.num_parents)
            )

    @tap
    @abstractmethod
    def produce(self, parents: Sequence[Genome], spawner: GeneSpawner) -> Genome:
        """Produce a child Genome from parent Genomes and optional GenomeSpawner.

        Parameters
        ----------
        parents
            A list of parent Genomes given to the operator.
        spawner
            A GeneSpawner that can be used to produce new genes (aka Atoms).

        """
        pass


class VariationStrategy(DiscreteProbDistrib):
    """A collection of VariationOperator and how frequently to use them."""

    def add(self, op: VariationOperator, p: float):
        """Add an element with a relative probability.

        Parameters
        ----------
        op : VariationOperator
            The VariationOperator to add to the variation strategy.
        p : float
            The probability of using the given operator relative to the other
            operators that have been added to the VariationStrategy.

        """
        super().add(op, p)


class VariationPipeline(VariationOperator):
    """Variation operator that sequentially applies multiple others variation operators.

    Parameters
    ----------
    operators : Sequence[VariationOperators]
        A list of operators to apply in order to produce the child Genome.

    Attributes
    ----------
    operators : Sequence[VariationOperators]
        A list of operators to apply in order to produce the child Genome.
    num_parents : int
        Number of parent Genomes the operator needs to produce a child
        Individual.

    """

    def __init__(self, operators: Sequence[VariationOperator]):
        num_parents_needed = max([op.num_parents for op in operators])
        super().__init__(num_parents_needed)
        self.operators = operators

    @tap
    def produce(self, parents: Sequence[Genome], spawner: GeneSpawner) -> Genome:
        """Produce a child Genome from parent Genomes and optional GenomeSpawner.

        Parameters
        ----------
        parents
            A list of parent Genomes given to the operator.
        spawner
            A GeneSpawner that can be used to produce new genes (aka Atoms).

        """
        super().produce(parents, spawner)
        self.checknum_parents(parents)
        child = parents[0]
        for op in self.operators:
            child = op.produce([child] + parents[1:], spawner)
        return child


# Utilities

def _gaussian_noise_factor():
    """Return Gaussian noise of mean 0, std dev 1.

    Returns
    --------
    Float samples from Gaussian distribution.

    Examples
    --------
    >>> gaussian_noise_factor()
    1.43412557975