// Package genetic provides functions to work with strings
// using genetic algorithm. https://en.wikipedia.org/wiki/Genetic_algorithm
//
// Author: D4rkia
package genetic

import (
	"errors"
	"fmt"
	"math/rand"
	"sort"
	"strconv"
	"time"
	"unicode/utf8"
)

// Population item represent a single step in the evolution process.
// One can think of population item as a single species.
// Key stands for the actual data entity of the species, which is a string
// in current implementation. Key can be interpreted as species DNA.
// Value shows how close this species to the desired target, where 1 means,
// that species DNA equals to the targeted one, 0 for no matchings in the DNA.
//
// **Note** In the current implementation species DNA length is suppose to be
// equal to the target length for algorithm to work.
type PopulationItem struct {
	Key   string
	Value float64
}

// Conf stands for cofigurations set provided to GeneticString function.
type Conf struct {
	// Maximum size of the population.
	// Bigger could be faster but more memory expensive.
	PopulationNum int

	// Number of elements selected in every generation for evolution
	// the selection takes. Place from the best to the worst of that
	// generation must be smaller than PopulationNum.
	SelectionNum int

	// Probability that an element of a generation can mutate changing one of
	// its genes this guarantees that all genes will be used during evolution.
	MutationProb float64

	// Enables debugging output to the console.
	Debug bool
}

// Result structure contains generation process statistics, as well as the
// best resulted population item.
type Result struct {
	// Number of generations steps performed.
	Generation int

	// Number of generated population items.
	Analyzed int

	// Result of generation with the best Value.
	Best PopulationItem
}

// GeneticString generates PopultaionItem based on the imputed target
// string, and a set of possible runes to build a string with. In order
// to optimise string generation additional configurations can be provided
// with Conf instance. Empty instance of Conf (&Conf{}) can be provided,
// then default values would be set.
//
// Link to the same algorithm implemented in python:
// https://github.com/TheAlgorithms/Python/blob/master/genetic_algorithm/basic_string.py
func GeneticString(target string, charmap []rune, conf *Conf) (*Result, error) {
	populationNum := conf.PopulationNum
	if populationNum == 0 {
		populationNum = 200
	}

	selectionNum := conf.SelectionNum
	if selectionNum == 0 {
		selectionNum = 50
	}

	// Verify if 'populationNum' s bigger than 'selectionNum'
	if populationNum < selectionNum {
		return nil, errors.New("populationNum must be bigger than selectionNum")
	}

	mutationProb := conf.MutationProb
	if mutationProb == .0 {
		mutationProb = .4
	}

	debug := conf.Debug

	// Just a seed to improve randomness required by the algorithm
	rand.Seed(time.Now().UnixNano())

	// Verify that the target contains no genes besides the ones inside genes variable.
	for position, r := range target {
		invalid := true
		for _, n := range charmap {
			if n == r {
				invalid = false
			}
		}
		if invalid {
			message := fmt.Sprintf("character not available in charmap at position: %v", position)
			return nil, errors.New(message)
		}
	}

	// Generate random starting population
	pop := make([]PopulationItem, populationNum)
	for i := 0; i < populationNum; i++ {
		key := ""
		for x := 0; x < utf8.RuneCountInString(target); x++ {
			choice := rand.Intn(len(charmap))
			key += string(charmap[choice])
		}
		pop[i] = PopulationItem{key, 0}
	}

	// Just some logs to know what the algorithms is doing
	gen, generatedPop := 0, 0

	// This loop will end when we will find a perfect match for our target
	for {
		gen++
		generatedPop += len(pop)

		// Random population created now it's time to evaluate
		for i, item := range pop {
			pop[i].Value = 0
			itemKey, targetRune := []rune(item.Key), []rune(target)
			for x := 0; x < len(target); x++ {
				if itemKey[x] == targetRune[x] {
					pop[i].Value++
				}
			}
			pop[i].Value = pop[i].Value / float64(len(targetRune))
		}
		sort.SliceStable(pop, func(i, j int) bool { return pop[i].Value > pop[j].Value })

		// Check if there is a matching evolution
		if pop[0].Key == target {
			break
		}

		// Print the best resultPrint the Best result every 10 generations
		// just to know that the algorithm is working
		if debug && gen%10 == 0 {
			fmt.Println("Generation:", strconv.Itoa(gen), "Analyzed:", generatedPop, "Best:", pop[0])
		}

		// Generate a new population vector keeping some of the best evolutions
		// Keeping this avoid regression of evolution
		var popChildren []PopulationItem
		popChildren = append(popChildren, pop[0:int(selectionNum/3)]...)

		// This is Selection
		for i := 0; i < int(selectionNum); i++ {
			parent1 := pop[i]
			// Generate more child proportionally to the fitness score
			nChild := (parent1.Value * 100) + 1
			if nChild >= 10 {
				nChild = 10
			}
			for x := 0.0; x < nChild; x++ {
				parent2 := pop[rand.Intn(selectionNum)]
				// Crossover
				split := rand.Intn(utf8.RuneCountInString(target))
				child1 := append([]rune(parent1.Key)[:split], []rune(parent2.Key)[split:]...)
				child2 := append([]rune(parent2.Key)[:split], []rune(parent1.Key)[split:]...)
				// Clean fitness value
				// Mutate
				if rand.Float64() < mutationProb {
					child1[rand.Intn(len(child1))] = charmap[rand.Intn(len(charmap))]
				}
				if rand.Float64() < mutationProb {
					child2[rand.Intn(len(child2))] = charmap[rand.Intn(len(charmap))]
				}
				// Push into 'popChildren'
				popChildren = append(popChildren, PopulationItem{string(child1), 0})
				popChildren = append(popChildren, PopulationItem{string(child2), 0})

				// Check if the population has already reached the maximum value and if so,
				// break the cycle. If this check is disabled the algorithm will take
				// forever to compute large strings but will also calculate small string in
				// a lot fewer generationsÃ¹
				if len(popChildren) >= selectionNum {
					break
				}
			}
		}
		pop = popChildren
	}
	return &Result{gen, generatedPop, pop[0]}, nil
}
