package ee.ut.ga.business;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.log4j.Logger;

import ee.ut.ga.model.PWM;
import ee.ut.ga.model.Weight;
import ee.ut.ga.util.ConstantsEnum;
import ee.ut.ga.util.MathUtil;
import ee.ut.ga.util.PwmUtil;

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

  /**
   * Counts fitness value of specified <code>PWM</code> in given data. Fitness value counts with the help of pValue.
   * Larger result means that <code>PWM</code> suits better to the final result of the project.
   */
  public static void setFitnessValue(Map data, PWM pwm, int GOOD_ALL, String FITNESS_FUNCTION) {

    List weights = calculateAllWeights(data, pwm);

    if (ConstantsEnum.FF___P_VALUE.equals(FITNESS_FUNCTION)) {
      getOptimalPValue(weights, pwm, GOOD_ALL);
    } else if (ConstantsEnum.FF___ROC.equals(FITNESS_FUNCTION)) {
      getOptimalRocArea(weights, pwm, GOOD_ALL);
    }

    log.debug("pValue parameters:\n" + "good - " + pwm.getDataGood() + ", goodAll is " + pwm.getDataGoodAll()
        + ", bad is " + pwm.getDataBad() + ", badAll is " + pwm.getDataBadAll() + ", pValue is " + pwm.getFitness()
        + ". Threshold is " + pwm.getWeightThreshold());
  }

  /**
   * Calculates all weights for specified Position-Weight-Matrix in given data.
   * 
   * @return <code>List</code> of <code>Weight</code>
   */
  public static List calculateAllWeights(Map data, PWM pwm) {
    log.debug("START > Get all weights for " + pwm.getPattern() + " from " + data.size() + " data rows");
    List allWeights = new ArrayList();
    int k = 0;
    for (Iterator i = data.entrySet().iterator(); i.hasNext();) {
      Entry entry = (Entry) i.next();
      String sequence = (String) entry.getValue();
      Weight sequenceMaxWeight = null;

      for (int j = 0; j < sequence.length() - pwm.getLength() + 1; j++) {
        BigDecimal value = PwmUtil.getValue(pwm.getPwm(), sequence.substring(j, j + pwm.getLength()));
        if (sequenceMaxWeight == null || value.doubleValue() > sequenceMaxWeight.getWeight()) {
          sequenceMaxWeight = new Weight(((Long) entry.getKey()).intValue(), j, value.doubleValue());
        }
      }

      if (sequenceMaxWeight == null)
        sequenceMaxWeight = new Weight(((Long) entry.getKey()).intValue(), 0, -1000000d);

      allWeights.add(sequenceMaxWeight);
      logCalculatingProcess(++k);
    }
    System.out.println();
    log.debug("DONE! > Get all weights for " + pwm.getPattern());
    return allWeights;
  }

  private static void logCalculatingProcess(int k) {
    if (k % 10 == 0)
      System.out.print(".");
    if (k % 1000 == 0)
      System.out.println(k);
  }

  private static void getOptimalRocArea(List weights, PWM pwm, int GOOD_ALL) {
    if (GOOD_ALL != 0) {
      pwm.setFitness(getRocArea(weights, GOOD_ALL));
      pwm.setDataGoodAll(GOOD_ALL);
      return;
    }

    BigDecimal area = new BigDecimal("0");
    for (int i = 1; i < weights.size(); i++) {
      BigDecimal temp = getRocArea(weights, i);
      if (area.compareTo(temp) == -1) {
        area = temp;
        pwm.setFitness(area);
        pwm.setDataGoodAll(i);
      }
    }
  }

  private static BigDecimal getRocArea(List weights, int GOOD_ALL) {
    BigDecimal area = new BigDecimal("0");

    Collections.sort(weights);
    
    BigDecimal x_step = new BigDecimal(1.0/(weights.size()-GOOD_ALL));
    BigDecimal y_step = new BigDecimal(1.0/GOOD_ALL);

    BigDecimal current_x = new BigDecimal("0");
    BigDecimal current_y = new BigDecimal("0");

    BigDecimal prev_x = current_x;
    BigDecimal prev_y = current_y;

    for (Iterator i = weights.iterator(); i.hasNext();) {
      Weight currentWeight = (Weight) i.next();
      if (currentWeight.getDataRow() >= GOOD_ALL) {
        current_x = prev_x.add(x_step);
        current_y = prev_y;
      } else {
        current_x = prev_x;
        current_y = prev_y.add(y_step);
        area = area.add((new BigDecimal("1").subtract(current_x)).multiply(current_y.subtract(prev_y)));
      }
      prev_x = current_x;
      prev_y = current_y;
    }

    return area;
  }

  private static void getOptimalPValue(List weights, PWM pwm, int GOOD_ALL) {
    log.debug("START > Get best pValue for " + pwm.getPattern());

    if (GOOD_ALL != 0)
      calculateOptimalPValue(weights, pwm, GOOD_ALL);
    else
      calculateOptimalPValue(weights, pwm);

    System.out.println();
    log.debug("DONE! > Get best pValue for " + pwm.getPattern());
  }

  /**
   * Calculates optimal pValue.
   */
  private static void calculateOptimalPValue(List weights, PWM pwm) {
    int k = 0;
    List goodAllList = new ArrayList(weights.subList(0, (int) (weights.size() * 0.2f)));
    List badAllList = new ArrayList(weights.subList(goodAllList.size(), weights.size()));
    for (Iterator i = badAllList.iterator(); i.hasNext();) {
      Weight weight = (Weight) i.next();
      if (badAllList.size() < goodAllList.size())
        break;

      for (Iterator j = weights.iterator(); j.hasNext();) {
        double threshold = ((Weight) j.next()).getWeight();
        int good = calculate(goodAllList, threshold);
        int bad = calculate(badAllList, threshold);
        int goodAll = goodAllList.size();
        int badAll = badAllList.size();
        setBestValue(pwm, good, goodAll, bad, badAll, threshold);
      }
      logCalculatingProcess(++k);

      goodAllList.add(weight);
      i.remove();
    }
  }

  /**
   * Calculates optimal pValue.
   */
  private static void calculateOptimalPValue(List weights, PWM pwm, int goodAll) {
    List goodAllList = new ArrayList(weights.subList(0, goodAll));
    List badAllList = new ArrayList(weights.subList(goodAllList.size(), weights.size()));
    int k = 0;
    for (Iterator j = weights.iterator(); j.hasNext();) {
      double threshold = ((Weight) j.next()).getWeight();
      int good = calculate(goodAllList, threshold);
      int bad = calculate(badAllList, threshold);
      int badAll = badAllList.size();
      setBestValue(pwm, good, goodAll, bad, badAll, threshold);
      logCalculatingProcess(++k);
    }
  }

  /**
   * Calculates quantity of DNA sequencies where maximum weight is not less than threshold.
   */
  private static int calculate(List weights, double threshold) {
    int result = 0;
    for (Iterator i = weights.iterator(); i.hasNext();) {
      Weight weight = (Weight) i.next();
      if (weight.getWeight() >= threshold)
        result++;
    }
    return result;
  }

  private static void setBestValue(PWM pwm, int good, int goodAll, int bad, int badAll, double threshold) {
    BigDecimal tempValue = pValue(good, goodAll, bad, badAll);
    if (pwm.getFitness() == null
        || (tempValue.compareTo(pwm.getFitness()) == 1
            && pValueIsPoositiveTrue(good, goodAll, bad, badAll, tempValue))) {
      pwm.setFitness(tempValue);
      pwm.setDataGoodAll(goodAll);
      pwm.setDataGood(good);
      pwm.setDataBadAll(badAll);
      pwm.setDataBad(bad);
      pwm.setWeightThreshold(threshold);
    }
  }
  
  private static boolean pValueIsPoositiveTrue(int good, int goodAll, int bad, int badAll, BigDecimal tempValue) {
    if (bad == 0 || good+1 > goodAll)
      return true;
    
    if (tempValue.compareTo(pValue(good+1,goodAll,bad-1,badAll)) == -1)
      return true;
    
    return false;
  }

  /**
   * Calculates pValue.
   */
  private static BigDecimal pValue(int goodInt, int goodAllInt, int badInt, int badAllInt) {
    BigDecimal good = new BigDecimal("" + goodInt);
    BigDecimal goodAll = new BigDecimal("" + goodAllInt);
    BigDecimal bad = new BigDecimal("" + badInt);
    BigDecimal badAll = new BigDecimal("" + badAllInt);

    return MathUtil.pValue(good, goodAll, bad, badAll);
  }
}
