package ee.ut.ga.business;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

import org.apache.log4j.Logger;

import ee.ut.ga.model.PWM;
import ee.ut.ga.util.DataLoaderUtil;
import ee.ut.ga.util.PropertyUtil;

public class GeneticAlgorithm {
  public static Logger log = Logger.getLogger(GeneticAlgorithm.class);

  private Map data = new HashMap();
  private Map priors = new HashMap();
  private List population = new ArrayList(); // List of PWMs

  private static int GOOD_ALL;
  private static int BEST_TO_LEAVE;
  private static int POPULATION_SIZE;
  private static int PWM_MIN_LENGTH;

  private static String FITNESS_FUNCTION;
  private static String INITIAL_PATH;
  private static String LOGGING_PATH;

  public GeneticAlgorithm(String propPath) throws Exception {
    readProperties(propPath);
  }

  private void readProperties(String propPath) throws Exception {
    Properties properties = PropertyUtil.loadProperties(propPath);
    
    INITIAL_PATH = properties.getProperty(PropertyUtil.INITIAL_PWMS);
    LOGGING_PATH = properties.getProperty(PropertyUtil.LOGGING);

    priors = PropertyUtil.loadPriors(properties);
    data = DataLoaderUtil.loadData(properties.getProperty(PropertyUtil.DATASET));
    population = DataLoaderUtil.loadInitialPopulation(INITIAL_PATH, priors);
    
    if (properties.getProperty(PropertyUtil.GOOD_ALL) != null)
      GOOD_ALL = Long.valueOf(properties.getProperty(PropertyUtil.GOOD_ALL)).intValue();
    BEST_TO_LEAVE = Long.valueOf(properties.getProperty(PropertyUtil.BEST_TO_LEAVE)).intValue();
    POPULATION_SIZE = Long.valueOf(properties.getProperty(PropertyUtil.POPULATION_SIZE)).intValue();
    PWM_MIN_LENGTH = Long.valueOf(properties.getProperty(PropertyUtil.PWM_MIN_LENGTH)).intValue();
    
    FITNESS_FUNCTION = properties.getProperty(PropertyUtil.FITNESS_FUNCTION);
  }

  /**
   * GA generation change.
   */
  public void generation() throws Exception {
    evaluate();
    select();
    reproduct();
  }

  /**
   * First step of GA: evaluation - calculate fitness value for each PWM in population.
   */
  private void evaluate() {
    for (Iterator i = population.iterator(); i.hasNext();) {
      PWM pwm = (PWM) i.next();
      log.debug("GA Evaluate: started to count fitness value for " + pwm.getPattern());
      if (pwm.getFitness() == null)
        FitnessFunction.setFitnessValue(data, pwm, GOOD_ALL, FITNESS_FUNCTION);
      log.debug("GA Evaluate: finished to count fitness value for " + pwm.getPattern());
    }
  }

  /**
   * Second step of GA: selection - get the best PWMs.
   */
  private void select() {
    Collections.sort(population);
    logPopulation();
    if (population.size() > BEST_TO_LEAVE)
      population = population.subList(0, BEST_TO_LEAVE);
  }

  /**
   * Third step of GA: reproduction - full population of parents by children.
   */
  private void reproduct() throws Exception {
    List children = new ArrayList();
    while ((population.size() + children.size()) < POPULATION_SIZE) {
      PWM childPWM = ReproductionMethods.reproduct(population, priors, PWM_MIN_LENGTH);
      if (isUniquePWM(childPWM, children))
        children.add(childPWM);
    }
    population.addAll(children);
  }

  private boolean isUniquePWM(PWM childPWM, List children) {
    for (Iterator i = population.iterator(); i.hasNext();) {
      PWM pwmInPopulation = (PWM) i.next();
      if (pwmInPopulation.getPattern().equals(childPWM.getPattern()))
        return false;
    }
    for (Iterator i = children.iterator(); i.hasNext();) {
      PWM pwmInChildren = (PWM) i.next();
      if (pwmInChildren.getPattern().equals(childPWM.getPattern()))
        return false;
    }
    return true;
  }

  private void logPopulation() {
    StringBuffer pwms = new StringBuffer("#").append(getFormattedDate()).append("\n");

    for (Iterator i = population.iterator(); i.hasNext();) {
      pwms.append(i.next());
    }

    log.info("\n" + pwms);

    DataLoaderUtil.writeResult(pwms.toString(), LOGGING_PATH, true);
    DataLoaderUtil.writeResult(pwms.toString(), INITIAL_PATH, false);
  }

  private String getFormattedDate() {
    Date date = Calendar.getInstance().getTime();
    return DateFormat.getDateInstance().getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, new Locale("en")).format(date);
  }
}
