//=============================================================================
// Copyright 2006-2010 Daniel W. Dyer
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//=============================================================================
package org.uncommons.watchmaker.framework.islands;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.uncommons.watchmaker.framework.CandidateFactory;
import org.uncommons.watchmaker.framework.EvaluatedCandidate;
import org.uncommons.watchmaker.framework.EvolutionEngine;
import org.uncommons.watchmaker.framework.EvolutionObserver;
import org.uncommons.watchmaker.framework.EvolutionUtils;
import org.uncommons.watchmaker.framework.EvolutionaryOperator;
import org.uncommons.watchmaker.framework.FitnessEvaluator;
import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine;
import org.uncommons.watchmaker.framework.PopulationData;
import org.uncommons.watchmaker.framework.SelectionStrategy;
import org.uncommons.watchmaker.framework.TerminationCondition;
import org.uncommons.watchmaker.framework.termination.GenerationCount;

/**
 * An implementation of island evolution in which multiple independent populations are evolved in
 * parallel with periodic migration of individuals between islands.
 * @param <T> The type of entity that is to be evolved.
 * @author Daniel Dyer
 */
public class IslandEvolution<T>
{
    private final List<EvolutionEngine<T>> islands;
    private final Migration migration;
    private final boolean naturalFitness;
    private final Random rng;

    private final Set<IslandEvolutionObserver<? super T>> observers
        = new CopyOnWriteArraySet<IslandEvolutionObserver<? super T>>();

    private List<TerminationCondition> satisfiedTerminationConditions;


    /**
     * Create an island system with the specified number of identically-configured islands.
     * If you want more fine-grained control over the configuration of each island, use the
     * {@link #IslandEvolution(List, Migration, boolean, Random)} constructor, which accepts
     * a list of pre-created islands (each is an instance of {@link EvolutionEngine}). 
     * @param islandCount The number of separate islands that will be part of the system.
     * @param migration A migration strategy for moving individuals between islands at the
     * end of an epoch.
     * @param candidateFactory Generates the initial population for each island.
     * @param evolutionScheme The evolutionary operator, or combination of evolutionary operators,
     * used on each island.
     * @param fitnessEvaluator The fitness function used on each island.
     * @param selectionStrategy The selection strategy used on each island.
     * @param rng A source of randomness, used by all islands.
     * @see #IslandEvolution(List, Migration, boolean, Random) 
     */
    public IslandEvolution(int islandCount,
                           Migration migration,
                           CandidateFactory<T> candidateFactory,
                           EvolutionaryOperator<T> evolutionScheme,
                           FitnessEvaluator<? super T> fitnessEvaluator,
                           SelectionStrategy<? super T> selectionStrategy,
                           Random rng)
    {
        this(createIslands(islandCount,
                           candidateFactory,
                           evolutionScheme,
                           fitnessEvaluator,
                           selectionStrategy,
                           rng),
             migration,
             fitnessEvaluator.isNatural(),
             rng);
    }


    /**
     * Create an island evolution system from a list of pre-configured islands.  This constructor
     * gives more control over the configuration of individual islands than the alternative constructor.
     * The other constructor should be used where possible to avoid having to explicitly create each
     * island.
     * @param islands A list of pre-configured islands.
     * @param migration A migration strategy for moving individuals between islands at the
     * end of an epoch.
     * @param naturalFitness If true, indicates that higher fitness values mean fitter
     * individuals.  If false, indicates that fitter individuals will have lower scores.
     * @param rng A source of randomness, used by all islands.
     * @see #IslandEvolution(int, Migration, CandidateFactory, EvolutionaryOperator, FitnessEvaluator,
     * SelectionStrategy, Random)
     */
    public IslandEvolution(List<EvolutionEngine<T>> islands,
                           Migration migration,
                           boolean naturalFitness,
                           Random rng)
    {
        this.islands = islands;
        this.migration = migration;
        this.naturalFitness = naturalFitness;
        this.rng = rng;

        for (int i = 0; i < islands.size(); i++)
        {
            final int islandIndex = i;
            EvolutionEngine<T> island = islands.get(islandIndex);
            island.addEvolutionObserver(new EvolutionObserver<T>()
            {
                public void populationUpdate(PopulationData<? extends T> populationData)
                {
                    for (IslandEvolutionObserver<? super T> islandObserver : observers)
                    {
                        islandObserver.islandPopulationUpdate(islandIndex, populationData);
                    }
                }
            });
        }
    }


    /**
     * Helper method used by the constructor to create the individual islands if they haven't
     * been provided already (via the other constructor).
     */
    private static <T> List<EvolutionEngine<T>> createIslands(int islandCount,
                                                              CandidateFactory<T> candidateFactory,
                                                              EvolutionaryOperator<T> evolutionScheme,
                                                              FitnessEvaluator<? super T> fitnessEvaluator,
                                                              SelectionStrategy<? super T> selectionStrategy,
                                                              Random rng)
    {
        List<EvolutionEngine<T>> islands = new ArrayList<EvolutionEngine<T>>(islandCount);
        for (int i = 0; i < islandCount; i++)
        {
            GenerationalEvolutionEngine<T> island = new GenerationalEvolutionEngine<T>(candidateFactory,
                                                                                       evolutionScheme,
                                                                                       fitnessEvaluator,
                                                                                       selectionStrategy,
                                                                                       rng);
            island.setSingleThreaded(true); // Don't need fine-grained concurrency when each island is on a separate thread.
            islands.add(island);
        }
        return islands;
    }


    /**
     * <p>Start the evolutionary process on each island and return the fittest candidate so far at the point
     * any of the termination conditions is satisfied.</p>
     *
     * <p><em>If you interrupt the request thread before this method returns, the
     * method will return prematurely (with the best individual found so far).
     * After returning in this way, the current thread's interrupted flag
     * will be set.  It is preferable to use an appropritate
     * {@link org.uncommons.watchmaker.framework.TerminationCondition} rather than interrupting the evolution in
     * this way.</em></p>
     *
     * @param populationSize The population size <em>for each island</em>.  Therefore, if you have 5 islands,
     * setting this parameter to 200 will result in 1000 individuals overall, 200 on each island.
     * @param eliteCount The number of candidates preserved via elitism <em>on each island</em>.  In elitism,
     * a sub-set of the population with the best fitness scores are preserved unchanged in
     * the subsequent generation.  Candidate solutions that are preserved unchanged through
     * elitism remain eligible for selection for breeding the remainder of the next generation.
     * This value must be non-negative and less than the population size.  A value of zero
     * means that no elitism will be applied.
     * @param epochLength The number of generations that make up an epoch.  Islands evolve independently for
     * this number of generations and then migration occurs at the end of the epoch and the next epoch starts.
     * @param migrantCount The number of individuals that will be migrated from each island at the end of each
     * epoch.
     * @param conditions One or more conditions that may cause the evolution to terminate.
     * @return The fittest solution found by the evolutionary process on any of the islands.
     */
    public T evolve(int populationSize,
                    int eliteCount,
                    int epochLength,
                    int migrantCount,
                    TerminationCondition... conditions)
    {
        ExecutorService threadPool = Executors.newFixedThreadPool(islands.size());
        List<List<T>> islandPopulations = new ArrayList<List<T>>(islands.size());
        List<EvaluatedCandidate<T>> evaluatedCombinedPopulation = new ArrayList<EvaluatedCandidate<T>>();

        PopulationData<T> data = null;
        List<TerminationCondition> satisfiedConditions = null;
        int currentEpochIndex = 0;
        long startTime = System.currentTimeMillis();
        while (satisfiedConditions == null)
        {
            List<Callable<List<EvaluatedCandidate<T>>>> islandEpochs = createEpochTasks(populationSize,
                                                                                        eliteCount,
                                                                                        epochLength,
                                                                                        islandPopulations);
            try
            {
                List<Future<List<EvaluatedCandidate<T>>>> futures = threadPool.invokeAll(islandEpochs);

                evaluatedCombinedPopulation.clear();
                List<List<EvaluatedCandidate<T>>> evaluatedPopulations
                    = new ArrayList<List<EvaluatedCandidate<T>>>(islands.size());
                for (Future<List<EvaluatedCandidate<T>>> future : futures)
                {
                    List<EvaluatedCandidate<T>> evaluatedIslandPopulation = future.get();
                    evaluatedCombinedPopulation.addAll(evaluatedIslandPopulation);
                    evaluatedPopulations.add(evaluatedIslandPopulation);
                }

                migration.migrate(evaluatedPopulations, migrantCount, rng);

                EvolutionUtils.sortEvaluatedPopulation(evaluatedCombinedPopulation, naturalFitness);
                data = EvolutionUtils.getPopulationData(evaluatedCombinedPopulation,
                                                        naturalFitness,
                                                        eliteCount,
                                                        currentEpochIndex,
                                                        startTime);
                notifyPopulationChange(data);

                islandPopulations.clear();
                for (List<EvaluatedCandidate<T>> evaluatedPopulation : evaluatedPopulations)
                {
                    islandPopulations.add(toCandidateList(evaluatedPopulation));
                }
                ++currentEpochIndex;
            }
            catch (InterruptedException ex)
            {
                Thread.currentThread().interrupt();
            }
            catch (ExecutionException ex)
            {
                throw new IllegalStateException(ex);
            }
            satisfiedConditions = EvolutionUtils.shouldContinue(data, conditions);
        }
        threadPool.shutdownNow();

        this.satisfiedTerminationConditions = satisfiedConditions;
        return evaluatedCombinedPopulation.get(0).getCandidate();
    }


    /**
     * Create the concurrently-executed tasks that perform evolution on each island.
     */
    private List<Callable<List<EvaluatedCandidate<T>>>> createEpochTasks(int populationSize,
                                                                         int eliteCount,
                                                                         int epochLength,
                                                                         List<List<T>> islandPopulations)
    {
        List<Callable<List<EvaluatedCandidate<T>>>> islandEpochs
            = new ArrayList<Callable<List<EvaluatedCandidate<T>>>>(islands.size());
        for (int i = 0; i < islands.size(); i++)
        {
            islandEpochs.add(new Epoch<T>(islands.get(i),
                                          populationSize,
                                          eliteCount,
                                          islandPopulations.isEmpty() ? Collections.<T>emptyList() : islandPopulations.get(i),
                                          new GenerationCount(epochLength)));
        }
        return islandEpochs;
    }


    /**
     * Convert a list of {@link EvaluatedCandidate}s into a simple list of candidates.
     * @param evaluatedCandidates The population of candidate objects to relieve of their
     * evaluation wrappers.
     * @param <T> The type of entity that is being evolved.
     * @return The candidates, stripped of their fitness scores.
     */
    private static <T> List<T> toCandidateList(List<EvaluatedCandidate<T>> evaluatedCandidates)
    {
        List<T> candidates = new ArrayList<T>(evaluatedCandidates.size());
        for (EvaluatedCandidate<T> evaluatedCandidate : evaluatedCandidates)
        {
            candidates.add(evaluatedCandidate.getCandidate());
        }
        return candidates;
    }


    /**
     * <p>Returns a list of all {@link TerminationCondition}s that are satisfied by the current
     * state of the island evolution.  Usually this list will contain only one item, but it
     * is possible that mutliple termination conditions will become satisfied at the same
     * time.  In this case the condition objects in the list will be in the same order that
     * they were specified when passed to the engine.</p>
     *
     * <p>If the evolution has not yet terminated (either because it is still in progress or
     * because it hasn't even been started) then an IllegalStateException will be thrown.</p>
     *
     * <p>If the evolution terminated because the request thread was interrupted before any
     * termination conditions were satisfied then this method will return an empty list.</p>
     *
     * @throws IllegalStateException If this method is invoked on an island system before
     * evolution is started or while it is still in progress.
     *
     * @return A list of statisfied conditions.  The list is guaranteed to be non-null.  The
     * list may be empty because it is possible for evolution to terminate without any conditions
     * being matched.  The only situation in which this occurs is when the request thread is
     * interrupted.
     */
    public List<TerminationCondition> getSatisfiedTerminationConditions()
    {
        if (satisfiedTerminationConditions == null)
        {
            throw new IllegalStateException("EvolutionEngine has not terminated.");
        }
        else
        {
            return Collections.unmodifiableList(satisfiedTerminationConditions);
        }
    }


    /**
     * <p>Adds an observer to the evolution.  Observers will receives two types of updates:
     * updates from each individual island at the end of each generation, and updates for
     * the combined global population at the end of each epoch.</p>
     *
     * <p>Updates are dispatched synchronously on the request thread.  Observers should
     * complete their processing and return in a timely manner to avoid holding up
     * the evolution.</p>
     *
     * @param observer The callback that will be notified at the end of each generation and epoch.
     *
     * @see #removeEvolutionObserver(IslandEvolutionObserver)
     */
    public void addEvolutionObserver(final IslandEvolutionObserver<? super T> observer)
    {
        observers.add(observer);
    }


    /**
     * Remove the specified observer.
     * @param observer The observer to remove (if it is registered).
     *
     * @see #addEvolutionObserver(IslandEvolutionObserver)
     */
    public void removeEvolutionObserver(final IslandEvolutionObserver<? super T> observer)
    {
        observers.remove(observer);
    }


    /**
     * Send the population data to all registered observers.
     * @param data Information about the current state of the population.
     */
    private void notifyPopulationChange(PopulationData<T> data)
    {
        for (IslandEvolutionObserver<? super T> observer : observers)
        {
            observer.populationUpdate(data);
        }
    }
}
