from typing import Optional

import numpy as np

from optuna._experimental import experimental
from optuna.samplers.nsgaii._crossovers._base import BaseCrossover
from optuna.study import Study


@experimental("3.0.0")
class SPXCrossover(BaseCrossover):
    """Simplex Crossover operation used by :class:`~optuna.samplers.NSGAIISampler`.

    Uniformly samples child individuals from within a single simplex
    that is similar to the simplex produced by the parent individual.
    For further information about SPX crossover, please refer to the following paper:

    - `Shigeyoshi Tsutsui and Shigeyoshi Tsutsui and David E. Goldberg and
      David E. Goldberg and Kumara Sastry and Kumara Sastry
      Progress Toward Linkage Learning in Real-Coded GAs with Simplex Crossover.
      IlliGAL Report. 2000.
      <https://www.researchgate.net/publication/2388486_Progress_Toward_Linkage_Learning_in_Real-Coded_GAs_with_Simplex_Crossover>`_

    Args:
        epsilon:
            Expansion rate. If not specified, defaults to ``sqrt(len(search_space) + 2)``.
    """

    n_parents = 3

    def __init__(self, epsilon: Optional[float] = None) -> None:

        self._epsilon = epsilon

    def crossover(
        self,
        parents_params: np.ndarray,
        rng: np.random.RandomState,
        study: Study,
        search_space_bounds: np.ndarray,
    ) -> np.ndarray:

        # https://www.researchgate.net/publication/2388486_Progress_Toward_Linkage_Learning_in_Real-Coded_GAs_with_Simplex_Crossover
        # Section 2 A Brief Review of SPX

        n = self.n_parents - 1
        G = np.mean(parents_params, axis=0)  # Equation (1).
        rs = np.power(rng.rand(n), 1 / (np.arange(n) + 1))  # Equation (2).

        epsilon = np.sqrt(len(search_space_bounds) + 2) if self._epsilon is None else self._epsilon
        xks = [G + epsilon * (pk - G) for pk in parents_params]  # Equation (3).

        ck = 0  # Equation (4).
        for k in range(1, self.n_parents):
            ck = rs[k - 1] * (xks[k - 1] - xks[k] + ck)

        child_params = xks[-1] + ck  # Equation (5).

        return child_params
