# encoding=utf8
import logging

import numpy as np

from niapy.algorithms.algorithm import Algorithm

logging.basicConfig()
logger = logging.getLogger('niapy.algorithms.modified')
logger.setLevel('INFO')

__all__ = ['ParameterFreeBatAlgorithm']


class ParameterFreeBatAlgorithm(Algorithm):
    r"""Implementation of Parameter-free Bat algorithm.

    Algorithm:
        Parameter-free Bat algorithm

    Date:
        2020

    Authors:
        Iztok Fister Jr.
        This implementation is based on the implementation of basic BA from niapy

    License:
        MIT

    Reference paper:
        Iztok Fister Jr., Iztok Fister, Xin-She Yang. Towards the development of a parameter-free bat algorithm . In: FISTER Jr., Iztok (Ed.), BRODNIK, Andrej (Ed.). StuCoSReC : proceedings of the 2015 2nd Student Computer Science Research Conference. Koper: University of Primorska, 2015, pp. 31-34.

    Attributes:
        Name (List[str]): List of strings representing algorithm name.

    See Also:
        * :class:`niapy.algorithms.Algorithm`

    """

    Name = ['ParameterFreeBatAlgorithm', 'PLBA']

    @staticmethod
    def info():
        r"""Get algorithms information.

        Returns:
            str: Algorithm information.

        See Also:
            * :func:`niapy.algorithms.Algorithm.info`

        """
        return r"""Iztok Fister Jr., Iztok Fister, Xin-She Yang. Towards the development of a parameter-free bat algorithm . In: FISTER, Iztok (Ed.), BRODNIK, Andrej (Ed.). StuCoSReC : proceedings of the 2015 2nd Student Computer Science Research Conference. Koper: University of Primorska, 2015, pp. 31-34."""

    def __init__(self, *args, **kwargs):
        """Initialize ParameterFreeBatAlgorithm."""
        super().__init__(80, *args, **kwargs)
        self.loudness = 0.9
        self.pulse_rate = 0.1

    def set_parameters(self, **kwargs):
        r"""Set the parameters of the algorithm.

        See Also:
            * :func:`niapy.algorithms.Algorithm.set_parameters`

        """
        super().set_parameters(population_size=80, **kwargs)
        self.loudness = 0.9
        self.pulse_rate = 0.1

    def init_population(self, task):
        r"""Initialize the initial population.

        Args:
            task (Task): Optimization task

        Returns:
            Tuple[numpy.ndarray, numpy.ndarray[float], Dict[str, Any]]:
                1. New population.
                2. New population fitness/function values.
                3. Additional arguments:
                    * velocities (numpy.ndarray[float]): Velocities

        See Also:
            * :func:`niapy.algorithms.Algorithm.init_population`

        """
        population, fitness, d = Algorithm.init_population(self, task)
        velocities = np.zeros((self.population_size, task.dimension))
        d.update({'velocities': velocities})
        return population, fitness, d

    def local_search(self, best, task, **_kwargs):
        r"""Improve the best solution according to the Yang (2010).

        Args:
            best (numpy.ndarray): Global best individual.
            task (Task): Optimization task.

        Returns:
            numpy.ndarray: New solution based on global best individual.

        """
        return task.repair(best + 0.001 * self.normal(0, 1, task.dimension))

    def run_iteration(self, task, population, population_fitness, best_x, best_fitness, **params):
        r"""Core function of Parameter-free Bat Algorithm.

        Args:
            task (Task): Optimization task.
            population (numpy.ndarray): Current population
            population_fitness (numpy.ndarray[float]): Current population fitness/function values
            best_x (numpy.ndarray): Current best individual
            best_fitness(float): Current best individual function/fitness value
            params (Dict[str, Any]): Additional algorithm arguments

        Returns:
            Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, float, Dict[str, Any]]:
                1. New population
                2. New population fitness/function values
                3. New global best solution
                4. New global best fitness/objective value
                5. Additional arguments:
                    * velocities (numpy.ndarray): Velocities

        """
        velocities = params.pop('velocities')
        upper, lower = task.upper, task.lower
        for i in range(self.population_size):
            frequency = ((upper[0] - lower[0]) / float(self.population_size)) * self.normal(0, 1)
            velocities[i] += (population[i] - best_x) * frequency
            if self.random() > self.pulse_rate:
                solution = self.local_search(best=best_x, task=task, i=i, Sol=population)
            else:
                solution = task.repair(population[i] + velocities[i], rng=self.rng)
            new_fitness = task.eval(solution)
            if (new_fitness <= population_fitness[i]) and (self.random() < self.loudness):
                population[i], population_fitness[i] = solution, new_fitness
            if new_fitness <= best_fitness:
                best_x, best_fitness = solution.copy(), new_fitness
        return population, population_fitness, best_x, best_fitness, {'velocities': velocities}
