// Domain specific classes for seeding and fitness functions. These classes are 
// used to define the problem domain and are passed to the GeneticAlgorithm class
// as parameters.

// Gene seeding class with a fixed value.
class Seed {
    constructor(value = 0) {
        this.value = value;
    }

    run() {
        return this.value;
    }
}

// Gene random seeding class.
class Seed_Random {
    constructor(max) {
        this.max = max;
    }

    run() {
        return random(this.max);
    }
}

// Returns sum of all genes in a chromosome.
class Fitness_Accumulate {
    run(genes) {
        let s = 0;
        for (let gene of genes)
            s += gene;
        return s;
    }
}

// Returns a measure in percentages that indicates the fitness of  
// a particular chromosome (genes). Result is proportional to the number of 
// 1s in the genes. Fittest chromosome has all genes equal 1.
class Fitness_Nbits {
    run(genes) {
        if (genes.length) {
            let s = 0;
            for (let gene of genes)
                s += gene;
            return 100 * s / genes.length;
        }
        return 0;
    }
}

// Returns a measure in percentages that indicates the fitness of  
// a particular chromosome (genes) as a solution of 8-Queens problem.
// Result is proportional to the number of non-attacking pairs of 
// queens. Implemented by counting the number of all horizontal and diagonal 
// threats. Note that number of vertical threats is always 0 because of 
// the gene representation. 
class Fitness_8queens {
    run(genes) {
        let size = genes.length;
        let max = (size - 1) * size / 2;
        // Check horizontal threats
        let counter = 0;
        for (let j = 0; j < size - 1; ++j)
            for (let i = j + 1; i < size; ++i)
                if (genes[j] - genes[i] == 0)
                    counter++;
        // Check diagonal threats
        for (let j = 0; j < size - 1; ++j)
            for (let i = j + 1; i < size; ++i)
                if (Math.abs(genes[j] - genes[i]) == i - j)
                    counter++;
        return Math.round(100 * (max - counter) / max);
    }
}


// Test 1 tests the fitness functions.
function test1() {
    let actual1 = new Fitness_Accumulate().run([1, 2, 3, 4, 5, 6, 7, 8]);
    let expected1 = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8;
    let actual2 = new Fitness_Nbits().run([0, 1, 0, 1, 0, 1, 0, 1]);
    let expected2 = 50;
    let actual3 = new Fitness_8queens().run([0, 1, 2, 3, 4, 5, 6, 7]);
    let expected3 = 0;
    let actual4 = new Fitness_8queens().run([4, 2, 0, 6, 1, 7, 5, 3]);
    let expected4 = 100;
    let actual = actual1 + ',' + actual2 + ',' + actual3 + ',' + actual4;
    let expected = expected1 + ',' + expected2 + ',' + expected3 + ',' + expected4;

    compareAndReport('Test 1', actual, expected);
}

// Test 2 tests the fitness function of a chromosome.
function test2() {
    let sizeOfChromosome = 8;
    let individual = new GeneticAlgorithm.Population.Individual(
        new Seed(0), new Fitness_Accumulate(), sizeOfChromosome);
    individual.setGenes([1, 2, 3, 4, 5, 6, 7, 8]);
    let actual = individual.getFitness();
    let expected = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8;

    compareAndReport('Test 2', actual, expected);
}

// Test 3 tests the fitness function of an individual.
function test3() {
    let sizeOfChromosome = 8;
    let seedValue = 3;
    let individual = new GeneticAlgorithm.Population.Individual(
        new Seed(seedValue), new Fitness_Accumulate(), sizeOfChromosome);
    let actual = individual.getFitness();
    let expected = seedValue * sizeOfChromosome;

    compareAndReport('Test 3', actual, expected);
}

// Test 4 tests the seeding of an individual.
function test4() {
    let sizeOfChromosome = 8;
    let Max = 10;
    let i = new GeneticAlgorithm.Population.Individual(
        new Seed_Random(Max), new Fitness_Accumulate(), sizeOfChromosome);
    let actual = (
        i.getGene(0) >= 0 && i.getGene(0) < Max &&
        i.getGene(1) >= 0 && i.getGene(1) < Max &&
        i.getGene(2) >= 0 && i.getGene(2) < Max &&
        i.getGene(3) >= 0 && i.getGene(3) < Max &&
        i.getGene(4) >= 0 && i.getGene(4) < Max &&
        i.getGene(5) >= 0 && i.getGene(5) < Max &&
        i.getGene(6) >= 0 && i.getGene(6) < Max &&
        i.getGene(7) >= 0 && i.getGene(7) < Max &&
        (
            i.getGene(0) != i.getGene(1) ||
            i.getGene(1) != i.getGene(2) ||
            i.getGene(2) != i.getGene(3) ||
            i.getGene(3) != i.getGene(4) ||
            i.getGene(4) != i.getGene(5) ||
            i.getGene(5) != i.getGene(6) ||
            i.getGene(6) != i.getGene(7)
        )
    );
    let expected = true;

    compareAndReport('Test 4', actual, expected);
}

// Test 5 tests the fittest individual in a population.
function test5() {
    let sizeOfChromosome = 3;
    let sizeOfPopulation = 5;
    let population = new GeneticAlgorithm.Population(
        new Seed(0), new Fitness_Accumulate(), sizeOfChromosome, sizeOfPopulation);
    population.getIndividual(0).setGenes([1, 2, 3]);
    population.getIndividual(1).setGenes([10, 20, 30]);
    population.getIndividual(2).setGenes([10000, 20000, 30000]);
    population.getIndividual(3).setGenes([100, 200, 300]);
    population.getIndividual(4).setGenes([1000, 2000, 3000]);
    population.updateFittest();
    let actual = population.getFittest().getFitness();
    let expected = 10000 + 20000 + 30000;

    compareAndReport('Test 5', actual, expected);
}

// Test 6 tests the selection of a new generation from a population.
function test6() {
    let sizeOfChromosome = 8;
    let Max = 2;
    let sizeOfPopulation = 10;
    let ga = new GeneticAlgorithm(new Seed_Random(Max), new Fitness_Nbits());
    let newGeneration = ga.selection(sizeOfChromosome, sizeOfPopulation);
    let actual = true;
    for (let i = 0; i < newGeneration.getSize(); ++i) {
        if (new Fitness_Nbits().run(newGeneration.getIndividual(i).getGenes()) !=
            newGeneration.getIndividual(i).getFitness()) {
            actual = false;
            break;
        }
    }
    let expected = true;

    compareAndReport('Test 6', actual, expected);
}

// Test 7 tests the crossover of a new generation from a population.
function test7() {
    let sizeOfChromosome = 8;
    let sizeOfPopulation = 2;
    let newGeneration = new GeneticAlgorithm.Population(
        new Seed(0), new Fitness_Nbits(), sizeOfChromosome, sizeOfPopulation);
    newGeneration.getIndividual(0).setGenes([0, 0, 0, 0, 0, 0, 0, 0]);
    newGeneration.getIndividual(1).setGenes([1, 1, 1, 1, 1, 1, 1, 1]);
    let ga = new GeneticAlgorithm(new Seed(0), new Fitness_Nbits());
    ga.crossover(newGeneration, sizeOfChromosome, GeneticAlgorithm.CrossoverMethod.Middle);
    let actual = newGeneration.getIndividual(0).toString();
    let expected = "[1,1,1,1,0,0,0,0]=50";

    compareAndReport('Test 7', actual, expected);
}

// Test 8 tests the run method of the genetic algorithm.
function test8() {
    let sizeOfChromosome = 8;
    let Max = 2;
    let sizeOfPopulation = 10;
    let mutationProbability = 70;
    let crossoverMethod = GeneticAlgorithm.CrossoverMethod.Middle;
    let ga = new GeneticAlgorithm(new Seed_Random(Max), new Fitness_Nbits());
    ga.run(sizeOfChromosome, sizeOfPopulation, mutationProbability, crossoverMethod);
    let actual = ga.getFittest().toString();
    let expected = "[1,1,1,1,1,1,1,1]=100";

    compareAndReport('Test 8', actual, expected);
}

// Test 9 tests the run method of the genetic algorithm for 8-Queens problem.
function test9() {
    let sizeOfChromosome = 8;
    let Max = 8;
    let sizeOfPopulation = 100;
    let mutationProbability = 70;
    let ga = new GeneticAlgorithm(new Seed_Random(Max), new Fitness_8queens());
    ga.run(sizeOfChromosome, sizeOfPopulation, mutationProbability, 
        GeneticAlgorithm.CrossoverMethod.Middle);
    let actual = new Fitness_8queens().run(ga.getFittest().getGenes());
    let expected = 100;

    compareAndReport('Test 9', actual, expected);
}

// Same as 9 but using GeneticAlgorithm.CrossoverMethod.Random.
function test10() {
    let sizeOfChromosome = 8;
    let Max = 8;
    let sizeOfPopulation = 100;
    let mutationProbability = 70;
    let ga = new GeneticAlgorithm(new Seed_Random(Max), new Fitness_8queens());
    ga.run(sizeOfChromosome, sizeOfPopulation, mutationProbability,  
        GeneticAlgorithm.CrossoverMethod.Random);
    let actual = new Fitness_8queens().run(ga.getFittest().getGenes());
    let expected = 100;

    compareAndReport('Test 10', actual, expected);
}