# encoding=utf8

import logging
from queue import Queue
from threading import Thread
from unittest import TestCase

from numpy import random as rnd, full, inf, array_equal, apply_along_axis, asarray

from NiaPy.util import objects2array
from NiaPy.task.task import Task, StoppingTask
from NiaPy.algorithms.algorithm import Individual, Algorithm
from NiaPy.benchmarks import Benchmark, Sphere

logging.basicConfig()
logger = logging.getLogger('NiaPy.test')
logger.setLevel('INFO')

class MyBenchmark(Benchmark):
	r"""Testing benchmark class.

	Date:
		April 2019

	Author:
		Klemen Berkovič

	See Also:
		* :class:`NiaPy.benchmarks.Benchmark`
	"""
	def __init__(self):
		Benchmark.__init__(self, -5.12, 5.12)

	def function(self):
		return Sphere(self.Lower, self.Upper).function()

class IndividualTestCase(TestCase):
	r"""Test case for testing Individual class.

	Date:
		April 2019

	Author:
		Klemen Berkovič

	See Also:
		* :class:`NiaPy.algorithms.Individual`
	"""
	def setUp(self):
		self.D = 20
		self.x, self.task = rnd.uniform(-100, 100, self.D), StoppingTask(D=self.D, nFES=230, nGEN=inf, benchmark=MyBenchmark())
		self.s1, self.s2, self.s3 = Individual(x=self.x, e=False), Individual(task=self.task, rand=rnd), Individual(task=self.task)

	def test_generateSolutin_fine(self):
		self.assertTrue(self.task.isFeasible(self.s2))
		self.assertTrue(self.task.isFeasible(self.s3))

	def test_evaluate_fine(self):
		self.s1.evaluate(self.task)
		self.assertAlmostEqual(self.s1.f, self.task.eval(self.x))

	def test_repair_fine(self):
		s = Individual(x=full(self.D, 100))
		self.assertFalse(self.task.isFeasible(s.x))

	def test_eq_fine(self):
		self.assertFalse(self.s1 == self.s2)
		self.assertTrue(self.s1 == self.s1)
		s = Individual(x=self.s1.x)
		self.assertTrue(s == self.s1)

	def test_str_fine(self):
		self.assertEqual(str(self.s1), '%s -> %s' % (self.x, inf))

	def test_getitem_fine(self):
		for i in range(self.D): self.assertEqual(self.s1[i], self.x[i])

	def test_len_fine(self):
		self.assertEqual(len(self.s1), len(self.x))

def init_pop_numpy(task, NP, **kwargs):
	r"""Custom population initialization function for numpy individual type.

	Args:
		task (Task): Optimization task.
		np (int): Population size.
		**kwargs (Dict[str, Any]): Additional arguments.

	Returns:
		Tuple[numpy.ndarray, numpy.ndarray[float]):
			1. Initialized population.
			2. Initialized populations fitness/function values.
	"""
	pop = full((NP, task.D), 0.0)
	fpop = apply_along_axis(task.eval, 1, pop)
	return pop, fpop

def init_pop_individual(task, NP, itype, **kwargs):
	r"""Custom population initialization function for numpy individual type.

	Args:
		task (Task): Optimization task.
		np (int): Population size.
		itype (Individual): Type of individual in population.
		**kwargs (Dict[str, Any]): Additional arguments.

	Returns:
		Tuple[numpy.ndarray, numpy.ndarray[float]):
			1. Initialized population.
			2. Initialized populations fitness/function values.
	"""
	pop = objects2array([itype(x=full(task.D, 0.0), task=task) for _ in range(NP)])
	return pop, asarray([x.f for x in pop])

class AlgorithBaseTestCase(TestCase):
	r"""Test case for testing Algorithm class.

	Date:
		April 2019

	Author:
		Klemen Berkovič

	Attributes:
		seed (int): Starting seed of random generator.
		rnd (mtrand.RandomState): Random generator.
		a (Algorithm): Algorithm to use for testing.

	See Also:
		* :class:`NiaPy.algorithms.Individual`
	"""
	def setUp(self):
		self.seed = 1
		self.rnd = rnd.RandomState(self.seed)
		self.a = Algorithm(seed=self.seed)

	def test_algorithm_info_fine(self):
		r"""Check if method works fine."""
		i = Algorithm.algorithmInfo()
		self.assertIsNotNone(i)

	def test_algorithm_getParameters_fine(self):
		r"""Check if method works fine."""
		algo = Algorithm()
		params = algo.getParameters()
		self.assertIsNotNone(params)

	def test_type_parameters_fine(self):
		d = Algorithm.typeParameters()
		self.assertIsNotNone(d)

	def test_init_population_numpy_fine(self):
		r"""Test if custome generation initialization works ok."""
		a = Algorithm(NP=10, InitPopFunc=init_pop_numpy)
		t = Task(D=20, benchmark=MyBenchmark())
		self.assertTrue(array_equal(full((10, t.D), 0.0), a.initPopulation(t)[0]))

	def test_init_population_individual_fine(self):
		r"""Test if custome generation initialization works ok."""
		a = Algorithm(NP=10, InitPopFunc=init_pop_individual, itype=Individual)
		t = Task(D=20, benchmark=MyBenchmark())
		i = Individual(x=full(t.D, 0.0), task=t)
		pop, fpop, d = a.initPopulation(t)
		for e in pop: self.assertEqual(i, e)

	def test_setParameters(self):
		self.a.setParameters(t=None, a=20)
		self.assertRaises(AttributeError, lambda: self.assertEqual(self.a.a, None))

	def test_randint_fine(self):
		o = self.a.randint(Nmax=20, Nmin=10, D=[10, 10])
		self.assertEqual(o.shape, (10, 10))
		self.assertTrue(array_equal(self.rnd.randint(10, 20, (10, 10)), o))
		o = self.a.randint(Nmax=20, Nmin=10, D=(10, 5))
		self.assertEqual(o.shape, (10, 5))
		self.assertTrue(array_equal(self.rnd.randint(10, 20, (10, 5)), o))
		o = self.a.randint(Nmax=20, Nmin=10, D=10)
		self.assertEqual(o.shape, (10,))
		self.assertTrue(array_equal(self.rnd.randint(10, 20, 10), o))

	def test_randn_fine(self):
		a = self.a.randn([1, 2])
		self.assertEqual(a.shape, (1, 2))
		self.assertTrue(array_equal(self.rnd.randn(1, 2), a))
		a = self.a.randn(1)
		self.assertEqual(len(a), 1)
		self.assertTrue(array_equal(self.rnd.randn(1), a))
		a = self.a.randn(2)
		self.assertEqual(len(a), 2)
		self.assertTrue(array_equal(self.rnd.randn(2), a))
		a = self.a.randn()
		self.assertIsInstance(a, float)
		self.assertTrue(array_equal(self.rnd.randn(), a))

	def test_uniform_fine(self):
		a = self.a.uniform(-10, 10, [10, 10])
		self.assertEqual(a.shape, (10, 10))
		self.assertTrue(array_equal(self.rnd.uniform(-10, 10, (10, 10)), a))
		a = self.a.uniform(4, 10, (4, 10))
		self.assertEqual(len(a), 4)
		self.assertEqual(len(a[0]), 10)
		self.assertTrue(array_equal(self.rnd.uniform(4, 10, (4, 10)), a))
		a = self.a.uniform(1, 4, 2)
		self.assertEqual(len(a), 2)
		self.assertTrue(array_equal(self.rnd.uniform(1, 4, 2), a))
		a = self.a.uniform(10, 100)
		self.assertIsInstance(a, float)
		self.assertEqual(self.rnd.uniform(10, 100), a)

	def test_normal_fine(self):
		a = self.a.normal(-10, 10, [10, 10])
		self.assertEqual(a.shape, (10, 10))
		self.assertTrue(array_equal(self.rnd.normal(-10, 10, (10, 10)), a))
		a = self.a.normal(4, 10, (4, 10))
		self.assertEqual(len(a), 4)
		self.assertEqual(len(a[0]), 10)
		self.assertTrue(array_equal(self.rnd.normal(4, 10, (4, 10)), a))
		a = self.a.normal(1, 4, 2)
		self.assertEqual(len(a), 2)
		self.assertTrue(array_equal(self.rnd.normal(1, 4, 2), a))
		a = self.a.normal(10, 100)
		self.assertIsInstance(a, float)
		self.assertEqual(self.rnd.normal(10, 100), a)

class TestingTask(StoppingTask, TestCase):
	r"""Testing task.

	Date:
		April 2019

	Author:
		Klemen Berkovič

	See Also:
		* :class:`NiaPy.util.StoppingTask`
	"""
	def names(self):
		r"""Get names of benchmark.

		Returns:
			 List[str]: List of task names.
		"""
		return self.benchmark.Name

	def eval(self, A):
		r"""Check if is algorithm trying to evaluate solution out of bounds."""
		self.assertTrue(self.isFeasible(A), 'Solution %s is not in feasible space!!!' % A)
		return StoppingTask.eval(self, A)

class AlgorithmTestCase(TestCase):
	r"""Base class for testing other algorithms.

	Date:
		April 2019

	Author:
		Klemen Berkovič

	Attributes:
		D (List[int]): Dimension of problem.
		nGEN (int): Number of generations/iterations.
		nFES (int): Number of function evaluations.
		seed (int): Starting seed of random generator.

	See Also:
		* :class:`NiaPy.algorithms.Algorithm`
	"""
	def setUp(self):
		r"""Setup basic parameters of the algorithm run."""
		self.D, self.nGEN, self.nFES, self.seed = [10, 40], 1000, 1000, 1
		self.algo = Algorithm

	def test_algorithm_type_parameters(self):
		r"""Test if type parametes for algorithm work fine."""
		tparams = self.algo.typeParameters()
		self.assertIsNotNone(tparams)

	def test_algorithm_info_fine(self):
		r"""Test if algorithm info works fine."""
		info = self.algo.algorithmInfo()
		self.assertIsNotNone(info)

	def test_algorithm_get_parameters_fine(self):
		r"""Test if algorithms parameters values are fine."""
		params = self.algo().getParameters()
		self.assertIsNotNone(params)

	def setUpTasks(self, D, bech='griewank', nFES=None, nGEN=None):
		r"""Setup optimization tasks for testing.

		Args:
			D (int): Dimension of the problem.
			bech (Optional[str, class]): Optimization problem to use.
			nFES (int): Number of fitness/objective function evaluations.
			nGEN (int): Number of generations.

		Returns:
			Tuple[Taks, Taks]: Two testing tasks.
		"""
		task1, task2 = TestingTask(D=D, nFES=self.nFES if nFES is None else nFES, nGEN=self.nGEN if nGEN is None else nGEN, benchmark=bech), TestingTask(D=D, nFES=self.nFES if nFES is None else nFES, nGEN=self.nGEN if nGEN is None else nGEN, benchmark=bech)
		return task1, task2

	def test_algorithm_run(self, a=None, b=None, benc='griewank', nFES=None, nGEN=None):
		r"""Run main testing of algorithm.

		Args:
			a (Algorithm): First instance of algorithm.
			b (Algorithm): Second instance of algorithm.
			benc (Union[Benchmark, str]): Benchmark to use for testing.
			nFES (int): Number of function evaluations.
			nGEN (int): Number of algorithm generations/iterations.
		"""
		if a is None or b is None: return
		for D in self.D:
			task1, task2 = self.setUpTasks(D, benc, nFES=nFES)
			q = Queue(maxsize=2)
			thread1, thread2 = Thread(target=lambda a, t, q: q.put(a.run(t)), args=(a, task1, q)), Thread(target=lambda a, t, q: q.put(a.run(t)), args=(b, task2, q))
			thread1.start(), thread2.start()
			thread1.join(), thread2.join()
			x, y = q.get(block=True), q.get(block=True)
			self.assertFalse(a.bad_run() or b.bad_run(), "Something went wrong at runtime of the algorithm --> %s" % a.exception)
			self.assertIsNotNone(x), self.assertIsNotNone(y)
			logger.info('%s\n%s -> %s\n%s -> %s' % (task1.names(), x[0], x[1], y[0], y[1]))
			self.assertAlmostEqual(task1.benchmark.function()(D, x[0].x if isinstance(x[0], Individual) else x[0]), x[1], msg='Best individual fitness values does not mach the given one')
			self.assertAlmostEqual(task1.x_f, x[1], msg='While running the algorithm, algorithm got better individual with fitness: %s' % task1.x_f)
			self.assertTrue(array_equal(x[0], y[0]), 'Results can not be reproduced, check usages of random number generator')
			self.assertAlmostEqual(x[1], y[1], msg='Results can not be reproduced or bad function value')
			self.assertTrue(self.nFES >= task1.Evals), self.assertEqual(task1.Evals, task2.Evals)
			self.assertTrue(self.nGEN >= task1.Iters), self.assertEqual(task1.Iters, task2.Iters)

# vim: tabstop=3 noexpandtab shiftwidth=3 softtabstop=3