#  Copyright (c) 2022, Manfred Moitzi
#  License: MIT License
from typing import List
import abc
from dataclasses import dataclass
import math
import random
import time

from . import genetic_algorithm as ga


class Temperature(abc.ABC):
    @abc.abstractmethod
    def get(self, step: float) -> float:
        ...


class LinearTemperature(Temperature):
    def __init__(self, start: float):
        self.start = start

    def get(self, step: float) -> float:
        return self.start * step


class Log:
    @dataclass
    class Entry:
        runtime: float
        temperature: float
        fitness: float

    def __init__(self):
        self.entries: List[Log.Entry] = []

    def add(self, runtime: float, temperature: float, fitness: float) -> None:
        self.entries.append(
            Log.Entry(
                runtime=runtime,
                fitness=fitness,
                temperature=temperature,
            )
        )


class SimulatedAnnealing:
    """Simulated annealing (SA) is a probabilistic technique for approximating
    the global optimum of a given function.

    Source: https://en.wikipedia.org/wiki/Simulated_annealing

    """

    def __init__(self, evaluator: ga.Evaluator):
        self.log = Log()
        self.min_fitness: float = 0.0
        self.start_time: float = 0.0
        self.mutation_rate: float = 0.1
        self.evaluator: ga.Evaluator = evaluator
        self.temperature: Temperature = LinearTemperature(500)
        self.mutation: ga.Mutate = ga.NeighborSwapMutate()
        # best result:
        self.best_fitness: float = 0.0
        self.best_dna: ga.DNA = ga.BitDNA([])

    def execute(self, candidate: ga.DNA, steps: int):
        self.start_time = time.perf_counter()
        temperature = self.temperature.get(1.0)
        self.best_fitness = self.evaluator.evaluate(candidate)
        self.best_dna = candidate
        self.log.add(0.0, temperature, self.best_fitness)

        for k in range(steps):
            step = 1.0 - (k + 1) / steps  # linear decreasing: 1.0 -> 0.0
            temperature = self.temperature.get(step)
            dna_new = self.neighbor(self.best_dna, step)
            fitness_new = self.evaluator.evaluate(dna_new)
            if fitness_new < self.best_fitness:
                self.add_log(fitness_new, temperature)
                self.best_fitness = fitness_new
                self.best_dna = dna_new
            else:
                p = self.probability(
                    self.best_fitness, fitness_new, temperature
                )
                if random.random() < p:
                    self.add_log(fitness_new, temperature)
                    self.best_fitness = fitness_new
                    self.best_dna = dna_new

            if self.best_fitness < self.min_fitness:
                break

    def probability(self, f0: float, f1: float, temperature: float) -> float:
        if temperature > 0.0:
            return math.exp(-abs(f1 - f0) / temperature)
        return 0.0

    def neighbor(self, candidate: ga.DNA, step: float) -> ga.DNA:
        candidate2 = candidate.copy()
        self.mutation.mutate(candidate2, step)
        return candidate2

    def add_log(self, fitness: float, temperature: float) -> None:
        self.log.add(
            time.perf_counter() - self.start_time,
            temperature,
            fitness,
        )

@@ -78,16 +78,26 @@
 ort List
+, Callable
 %0Aimport 
@@ -500,16 +500,459 @@
  step%0A%0A%0A
+PFunction = Callable%5B%5Bfloat, float, float%5D, float%5D%0A%0A%0Adef default_probability(e0: float, e1: float, temperature: float) -%3E float:%0A    %22%22%22Compute transition probability.%0A%0A    Args:%0A        e0: current lowest energy state (best fitness)%0A        e1: new energy state%0A        temperature: system temperature%0A%0A    %22%22%22%0A    if e1 %3C e0:%0A        return 1.0%0A    if temperature %3E 0.0:%0A        return math.exp(-abs(e1 - e0) / temperature)%0A    return 0.0%0A%0A%0A
 class Lo
@@ -1986,22 +1986,57 @@
     
-# best result:
+self.probability: PFunction = default_probability
 %0A   
@@ -2222,56 +2222,8 @@
 r()%0A
-        temperature = self.temperature.get(1.0)%0A
     
@@ -2341,16 +2341,21 @@
 dd(0.0, 
+self.
 temperat
@@ -2357,16 +2357,25 @@
 perature
+.get(1.0)
 , self.b
@@ -2509,68 +2509,15 @@
     
-temperature = self.temperature.get(step)%0A            dna_new
+new_dna
  = s
@@ -2562,27 +2562,27 @@
         
+new_
 fitness
-_new
  = self.
@@ -2600,23 +2600,23 @@
 valuate(
-dna_new
+new_dna
 )%0A      
@@ -2625,224 +2625,81 @@
     
-if fitness_new %3C self.best_fitness:%0A                self.add_log(fitness_new, temperature)%0A                self.best_fitness = fitness_new%0A                self.best_dna = dna_new%0A            else:%0A                p =
+temperature = self.temperature.get(step)%0A            if random.random() %3C
  sel
@@ -2713,20 +2713,16 @@
 bility(%0A
-    
         
@@ -2748,27 +2748,27 @@
 itness, 
+new_
 fitness
-_new
 , temper
@@ -2789,52 +2789,9 @@
     
-    )%0A                if random.random() %3C p
+)
 :%0A  
@@ -2800,28 +2800,24 @@
             
-    
 self.add_log
@@ -2817,27 +2817,27 @@
 add_log(
+new_
 fitness
-_new
 , temper
@@ -2839,28 +2839,24 @@
 emperature)%0A
-    
             
@@ -2883,24 +2883,20 @@
 s = 
+new_
 fitness
-_new%0A    
+%0A
     
@@ -2927,15 +2927,15 @@
 a = 
-dna_new
+new_dna
 %0A%0A  
@@ -3012,193 +3012,8 @@
 ak%0A%0A
-    def probability(self, f0: float, f1: float, temperature: float) -%3E float:%0A        if temperature %3E 0.0:%0A            return math.exp(-abs(f1 - f0) / temperature)%0A        return 0.0%0A%0A
     

refactor simulated annealing optimizer