package ee.ut.ga.business;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Map.Entry;

import org.apache.log4j.Logger;

import ee.ut.ga.model.MatrixKey;
import ee.ut.ga.model.PWM;
import ee.ut.ga.util.ConstantsEnum;
import ee.ut.ga.util.PwmUtil;

public class ReproductionMethods {
  public static Logger log = Logger.getLogger(ReproductionMethods.class);
  
  /**
   * Randomly chooses one of reproduction methods. 
   */
  public static PWM reproduct(List parents, Map priors, int PWM_MIN_LENGTH) throws Exception {
    int randomMutation = new Random().nextInt(2);

    if (randomMutation == 0) {
      return getRandom(parents, null, priors, PWM_MIN_LENGTH);
    }
    return multipleReproduction(parents, priors, PWM_MIN_LENGTH);
  }
  
  private static PWM getRandom(List parents, PWM parentPWM, Map priors, int PWM_MIN_LENGTH) throws Exception {
    int randomMutation = new Random().nextInt(5);
    
    if (parentPWM == null)
      parentPWM = (PWM) parents.get(new Random().nextInt(parents.size()));
    
    if (parentPWM.getLength() > PWM_MIN_LENGTH && randomMutation == 0) {
      return deleteColumn(parentPWM, priors);
    } else if (randomMutation == 1) {
      PWM parentPWM2 = (PWM) parents.get(new Random().nextInt(parents.size()));
      return onePointCrossover(parentPWM, parentPWM2, priors);
    } else if (randomMutation == 2) {
      PWM parentPWM2 = (PWM) parents.get(new Random().nextInt(parents.size()));
      return twoPointCrossover(parentPWM, parentPWM2, priors);
    } else if (randomMutation == 3) {
      PWM parentPWM2 = (PWM) parents.get(new Random().nextInt(parents.size()));
      return threePointCrossover(parentPWM, parentPWM2, priors);
    } else if (randomMutation == 4) {
      PWM parentPWM2 = (PWM) parents.get(new Random().nextInt(parents.size()));
      return fourPointCrossover(parentPWM, parentPWM2, priors);
    } else if (randomMutation == 5) {
      return inversionMutation(parentPWM, priors);
    }
    return addColumn(parentPWM, priors);
  }
  
  /**
   * Reproducts new PWM using random parent and random reproduction method.
   */
  private static PWM multipleReproduction(List parents, Map priors, int PWM_MIN_LENGTH) throws Exception {
    int mutationTimes = new Random().nextInt(10);
    
    PWM result = getRandom(parents, null, priors, PWM_MIN_LENGTH);
    for (int i = 0; i < (mutationTimes-1); i++) {
      result = getRandom(parents, result, priors, PWM_MIN_LENGTH);
    }
    return result;
  }
  
  /**
   * Deletes column in PWM at the specified position. 
   */
  private static PWM deleteColumn(PWM parentPWM, int position, Map priors) throws Exception {
    Map pam = (HashMap) ((HashMap) parentPWM.getPam()).clone();
    for (int i = 0; i < ConstantsEnum.NUCLEOTIDES.length; i++) {
      pam.remove(new MatrixKey(ConstantsEnum.NUCLEOTIDES[i], position));
    }
    for (int j = position; j < parentPWM.getLength(); j++) {
      for (int i = 0; i < ConstantsEnum.NUCLEOTIDES.length; i++) {
        pam.put(new MatrixKey(ConstantsEnum.NUCLEOTIDES[i], j), pam.get(new MatrixKey(ConstantsEnum.NUCLEOTIDES[i], j + 1)));
      }
    }
    for (int i = 0; i < ConstantsEnum.NUCLEOTIDES.length; i++) {
      pam.remove(new MatrixKey(ConstantsEnum.NUCLEOTIDES[i], parentPWM.getLength() - 1));
    }
    
    return new PWM(pam, priors);
  }

  /**
   * Deletes random column in PWM. 
   */
  private static PWM deleteColumn(PWM parentPWM, Map priors) throws Exception {
    return deleteColumn(parentPWM, new Random().nextInt(parentPWM.getLength()), priors);
  }

  /**
   * Adds column into PWM at the specified position. 
   */
  private static PWM addColumn(PWM parentPWM, int position, Map priors) throws Exception {
    Map pam = (HashMap) ((HashMap) parentPWM.getPam()).clone();
    for (int j = parentPWM.getLength() - 1; j > position; j--) {
      for (int i = 0; i < ConstantsEnum.NUCLEOTIDES.length; i++) {
        pam.put(new MatrixKey(ConstantsEnum.NUCLEOTIDES[i], j), pam.get(new MatrixKey(ConstantsEnum.NUCLEOTIDES[i], j - 1)));
      }
    }
    
    int quantityOfDNAs = PwmUtil.getMotifSetLength(parentPWM.getPam());
    int[] addition = getMultiDistribution(quantityOfDNAs, ConstantsEnum.NUCLEOTIDES.length);
    
    for (int i = 0; i < ConstantsEnum.NUCLEOTIDES.length; i++) {
      pam.put(new MatrixKey(ConstantsEnum.NUCLEOTIDES[i], position), new Long(addition[i]));
    }

    return new PWM(pam, priors);
  }

  /**
   * Adds column into PWM at random position. 
   */
  private static PWM addColumn(PWM parentPWM, Map priors) throws Exception {
    return addColumn(parentPWM, new Random().nextInt(parentPWM.getLength()), priors);
  }
  
  private static PWM onePointCrossover(PWM pwm1, PWM pwm2, Map priors) throws Exception {
    return uniformCrossover(pwm1, pwm2, priors, 1);
  }
  
  private static PWM twoPointCrossover(PWM pwm1, PWM pwm2, Map priors) throws Exception {
    return uniformCrossover(pwm1, pwm2, priors, 2);
  }
  
  private static PWM threePointCrossover(PWM pwm1, PWM pwm2, Map priors) throws Exception {
    return uniformCrossover(pwm1, pwm2, priors, 3);
  }
  
  private static PWM fourPointCrossover(PWM pwm1, PWM pwm2, Map priors) throws Exception {
    return uniformCrossover(pwm1, pwm2, priors, 4);
  }
  
  private static PWM uniformCrossover(PWM pwm1, PWM pwm2, Map priors, int crossoverPointNumbers) throws Exception {
    Map pam = new HashMap();
    
    int minLength = pwm2.getLength();
    if (pwm1.getLength() < pwm2.getLength())
      minLength = pwm1.getLength();
    int[] parts = getMultiDistribution(minLength, crossoverPointNumbers+1);
    int prev = 0;
    
    for (int i = 0; i < parts.length; i++) {
      if (i % 2 == 0 && pwm1.getLength() >= prev+parts[i])
        fillPAM(pam, pwm1, prev, prev+parts[i]);
      else if (pwm2.getLength() >= prev+parts[i])
        fillPAM(pam, pwm2, prev, prev+parts[i]);
      else
        fillPAM(pam, pwm1, prev, prev+parts[i]);
      
      prev += parts[i];
    }
    
    return new PWM(pam, priors);
  }
  
  private static void fillPAM(Map pam, PWM pwm, int startColumn, int endColumn) {
    for (int i = startColumn; i < endColumn; i++) {
      for (int j = 0; j < ConstantsEnum.NUCLEOTIDES.length; j++) {
        MatrixKey key = new MatrixKey(ConstantsEnum.NUCLEOTIDES[j], i);
        pam.put(key, pwm.getPam().get(key));
      }
    }
  }
  
  private static PWM inversionMutation(PWM parent, Map priors) throws Exception {
    Map pam = new HashMap();
    for (Iterator i = parent.getPam().entrySet().iterator(); i.hasNext();) {
      Entry entry = (Entry) i.next();
      pam.put(mutateKey((MatrixKey) entry.getKey()), entry.getValue());
    }
    return new PWM(pam, priors);
  }
  
  private static MatrixKey mutateKey(MatrixKey key) {
    String parent = key.getRow();
    if ("A".equalsIgnoreCase(parent))
      return new MatrixKey("T", key.getColumn());
    if ("C".equalsIgnoreCase(parent))
      return new MatrixKey("G", key.getColumn());
    if ("T".equalsIgnoreCase(parent))
      return new MatrixKey("A", key.getColumn());
    return new MatrixKey("C", key.getColumn());
  }
  
  private static int[] getMultiDistribution(int sum, int parts) {
    int[] randoms = new int[parts];
    for (int i = 0; i < sum; i++) {
      randoms[new Random().nextInt(parts)]++;
    }
    return randoms;
  }
}
