import numpy as np

class FinessEvaluator(object):
	def __init__(self):
		self.x = np.linspace(-10,10,21)
		pass

	def process(self, c):
		mse = 0
		for x in self.x:
			y = c.process(x)
			mse += (y - self.fx(x))**2

		mse *= 1/len(self.x)
		fit = 1/(1 + mse)
		return fit

	def fx(self, x):
		y = 5 + 3 * x + 7 * x**2
		return y

class Cromosome(object):
	def __init__(self, id, param=None):
		self.id = id
		self.fit = 0
		self.fitAccum = 0
		self.param = param

	def process(self, x):
		result = self.param[0] + self.param[1]*x + self.param[2]*x**2
		return result[0]

	def cross(self, p2):
		a = 0.5
		param1 = (1-a)*self.param + a*p2.param
		param2 = (1-a)*p2.param + a*self.param
		ca = Cromosome(self.id, param1)
		cb = Cromosome(p2.id, param2)
		return ca, cb

	def mutate(self):
		gene = int(np.random.rand() * 3)
		allele = int(5*(0.5 - np.random.rand()))
		self.param[gene] += allele

	def __str__(self):
		return "Id: {}, f={:0.6f}, fitAccum={:0.6f}, param=\n{}".format(self.id, self.fitness, self.fitAccum, str(self.param))


class Population(object):
	def __init__(self, numCromosomes):
		self.bestFit = 0
		self.sumFit = 0
		self.cromosomes = []
		for i in range(0, numCromosomes):
			self.cromosomes.append(Cromosome(str(i), np.random.rand(3,1)))

	def print(self):
		print ("Population fitness  = {:0.6f}".format(self.sumFit))
		print ("Best indiv. fitness = {:0.6f}".format(self.bestFit))

	def process(self):
		for c in self.cromosomes:
			y = c.process(1)
		return

	def fitness(self, fitnessEval):
		self.sumFit = 0
		self.bestFit = 0
		for c in self.cromosomes:
			f = fitnessEval.process(c)
			c.fitness = f
			self.sumFit += f
			if f > self.bestFit:
				self.bestFit = f

		# 1.) Sort by fitness
		sorted = self.sort(self.cromosomes)

		# 2.) Assign accumulated fitness
		sum = 0
		for c in sorted:
			sum += c.fitness
			c.fitAccum = sum

		self.cromosomes = sorted

		return self.sumFit

	def mate(self, chanceOfReproduction=0.3):
		newPop = self.cromosomes
		count = 0
		for i in range(0, int(len(self.cromosomes)/2)):
			p = np.random.rand()

			if p < chanceOfReproduction:
				# 3.) Select parents
				p1 = self.select(self.cromosomes)

				p2 = p1
				while(p2 == p1):
					p2 = self.select(self.cromosomes)

				ca, cb = p1.cross(p2)
				newPop[count+0] = ca
				newPop[count+1] = cb
				count += 2

		self.cromosomes = newPop
		return newPop

	def select(self, sorted):
		p = self.sumFit * np.random.rand()
		for c in sorted:
			if c.fitAccum > p:
				break

		return c

	def mutate(self, chanceOfMutation=0.06):
		for c in self.cromosomes:
			p = np.random.rand()

			if p < chanceOfMutation:
				c.mutate()

		pass

	def best(self):
		return self.cromosomes[len(self.cromosomes)-1]

	def starve(self):
		pass

	def sort(self, array):
		less = []
		equal = []
		greater = []

		if len(array) > 1:
			pivot = array[0].fitness
			for x in array:
				if x.fitness < pivot:
					less.append(x)
				elif x.fitness == pivot:
					equal.append(x)
				elif x.fitness > pivot:
					greater.append(x)
			# Don't forget to return something!
			return self.sort(less)+equal+self.sort(greater)  # Just use the + operator to join lists
		# Note that you want equal ^^^^^ not pivot
		else:  # You need to hande the part at the end of the recursion - when you only have one element in your array, just return the array.
			return array


if __name__ == '__main__':
	f = FinessEvaluator()
	p = Population(50)

	for i in range(0, 500):
		p.mutate(0.1)
		p.fitness(f)
		p.mate()
		p.print()

	p.fitness(f)
	p.print()
	print(p.best())
	print ("End of Program")