import numpy as np

from sklearn.neighbors import KDTree


class VoronoiTiles:
    """
    Class implementing voronoi tiling. For each point in the state space,
    this class can be used to compute the index of the corresponding tile.

    """
    def __init__(self, prototypes):
        """
        Constructor.

        Args:
            prototypes (list): list of prototypes to compute the partition.

        """
        self._size = len(prototypes)
        self._tree = KDTree(prototypes, leaf_size=1)

    def __call__(self, x):
        return self._tree.query(np.atleast_2d(x), return_distance=False).item()

    @staticmethod
    def generate(n_tilings, n_prototypes, low=None, high=None, mu=None, sigma=None):
        """
        Factory method to build ``n_tilings`` tilings of ``n_prototypes``.
        Prototypes are generated randomly sampled. If low and high are provided,
        prototypes are sampled uniformly between low and high, otherwise mu and
        sigma must be specified and prototypes are sampled from the corresponding
        Gaussian.

        Args:
            n_tilings (int): number of tilings, or -1 to compute the number
                             automatically;
            n_prototypes (list): number of prototypes for each tiling;
            low (np.ndarray, None): lowest value for each dimension, needed for
                                    uniform sampling;
            high (np.ndarray, None): highest value for each dimension, needed for
                                     uniform sampling.
            mu (np.ndarray, None): mean value for each dimension, needed for
                                   Gaussian sampling.
            sigma (np.ndarray, None): variance along each dimension, needed for
                                      Gaussian sampling.

        Returns:
            The list of the generated tiles.

        """

        assert (low is not None and high is not None and mu is None and sigma is None) \
               or (mu is not None and sigma is not None and low is None and high is None)

        uniform = low is not None

        if uniform:
            assert len(low) == len(high)
        else:
            assert len(mu) == len(sigma)

        assert n_tilings > 0 or n_tilings == -1

        if n_tilings == -1:
            d = np.size(low)  # space-dim
            m = np.max([np.ceil(np.log(4 * d) / np.log(2)),
                        np.ceil(np.log(n_prototypes) / np.log(2))])
            n_tilings = int(m**2)

        if uniform:
            low = np.array(low, dtype=np.float)
            high = np.array(high, dtype=np.float)
        else:
            mu = np.array(mu, dtype=np.float)
            sigma = np.array(sigma, dtype=np.float)

        # Generate the list of tilings
        tilings = list()

        for i in range(n_tilings):
            if uniform:
                prototypes = np.random.uniform(low, high, (n_prototypes, len(low)))
            else:
                prototypes = np.random.normal(mu, sigma, (n_prototypes, len(mu)))
            tilings.append(VoronoiTiles(prototypes))

        return tilings

    @property
    def size(self):
        return self._size
