package me.prettyprint.cassandra.connection;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import me.prettyprint.cassandra.service.CassandraHost;
import me.prettyprint.cassandra.utils.DaemonThreadPoolFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * This LB Algorithm has the Phi algo which Dynamic snitch uses, LB is based on the probablity of failure of the node.
 * TODO: Make cassandra code abstracted enough so we can inherit from the same.
 * 
 * @author Vijay Parthasarathy
 */
public class DynamicLoadBalancingPolicy implements LoadBalancingPolicy {

  private static final long serialVersionUID = -1044985880174118325L;
  private static final Logger log = LoggerFactory.getLogger(DynamicLoadBalancingPolicy.class);
  
  private final ScheduledExecutorService tasks = new ScheduledThreadPoolExecutor(1, new DaemonThreadPoolFactory(getClass()));

  // references which is used to make the real time requests faster.
  private Map<HClientPool, Double> scores = Maps.newConcurrentMap();
  private List<LatencyAwareHClientPool> allPools = new CopyOnWriteArrayList<LatencyAwareHClientPool>();

  // default values this can be changed by the Client.
  private int UPDATE_INTERVAL = 100;
  private int RESET_INTERVAL = 20000;
  private double DYNAMIC_BADNESS_THRESHOLD = 0.10;

  public DynamicLoadBalancingPolicy() {

    // Pre-calculate the scores so as we can compare it fast.
    Runnable updateThread = new Runnable() {
      public void run() {
        try {
          updateScores();
        } catch(Exception e) {
          log.info("exception updating scores", e);
        }
      }
    };

    // Clear Stats.
    Runnable resetThread = new Runnable() {
      public void run() {
        try {
          for (LatencyAwareHClientPool pool : allPools) {
            pool.clear();
          }
        } catch(Exception e) {
          log.info("exceotuib reseting stats", e);
        }
      }
    };
    tasks.scheduleWithFixedDelay(updateThread, UPDATE_INTERVAL, UPDATE_INTERVAL, TimeUnit.MILLISECONDS);
    tasks.scheduleWithFixedDelay(resetThread, RESET_INTERVAL, RESET_INTERVAL, TimeUnit.MILLISECONDS);
  }

  @Override
  public HClientPool getPool(Collection<HClientPool> pools, Set<CassandraHost> excludeHosts) {
    List<HClientPool> poolList = Lists.newArrayList(pools);

    // remove the hosts from the list.
    if (excludeHosts != null) {
      filter(poolList, excludeHosts);
    }

    Collections.shuffle(poolList);
    HClientPool fp = poolList.get(0);
    Double first = scores.get(fp);
    for (int i = 1; i < poolList.size(); i++) {
      HClientPool np = poolList.get(i);
      Double next = scores.get(np);
      if ((first - next) / first > DYNAMIC_BADNESS_THRESHOLD) {
        Collections.sort(poolList, new SortByScoreComparator());
        if (log.isDebugEnabled())
          log.debug(String.format("According to score we have chosen {} vs first {}", poolList.get(0), fp));
        break;
      }
    }
    return poolList.get(0);
  }

  private void filter(List<HClientPool> from, Set<CassandraHost> subList) {
    Iterator<HClientPool> it = from.iterator();
    while (it.hasNext()) {
      if (subList.contains(it.next().getCassandraHost()))
        it.remove();
    }
  }

  private class SortByScoreComparator implements Comparator<HClientPool> {
    public int compare(HClientPool p1, HClientPool p2) {
      Double scored1 = scores.get(p1);
      Double scored2 = scores.get(p2);
      if (scored1.equals(scored2))
        return 0;
      if (scored1 < scored2)
        return -1;
      else return 1;
    }
  }

  @Override
  public HClientPool createConnection(CassandraHost host) {
    LatencyAwareHClientPool pool = new LatencyAwareHClientPool(host);
    add(pool);
    return pool;
  }

  // This is helper class for the test cases. TODO: cleanup.
  void add(LatencyAwareHClientPool pool) {
    allPools.add(pool);
    // update the reference of the scores intially it is Zero.
    scores.put(pool, 0.0);
  }

  // This will be a expensive call.
  void updateScores() {
    for (LatencyAwareHClientPool pool : allPools) {
      scores.put(pool, pool.score());
      pool.resetIntervel();
    }
  }

  public int getUpdateInterval() {
    return UPDATE_INTERVAL;
  }

  /**
   * Set the configured interval for the stats to be recalculated (until this time it is been cached.
   * 
   * @param updateInterval
   *          In ms.
   */
  public void setUpdateInterval(int updateInterval) {
    UPDATE_INTERVAL = updateInterval;
  }

  public int getResetInterval() {
    return RESET_INTERVAL;
  }

  /**
   * Set the configured interval for the stats to be reset so that the new stats are allowed and we can get rid of bad
   * nodes value. This is under the assumption that the bad nodes will eventually get better....
   * 
   * @param resetInterval
   *          in ms
   */
  public void setResetInterval(int resetInterval) {
    RESET_INTERVAL = resetInterval;
  }

  public double getBadnessThreshold() {
    return DYNAMIC_BADNESS_THRESHOLD;
  }

  /**
   * This is the percentage of badness which is acceptable...
   * 
   * Example: A should be 0.20 (20%) bad than B before B is choosen rathar than A.
   * 
   * @param badness
   *          in %
   */
  public void setBadnessThreshold(double badness) {
    DYNAMIC_BADNESS_THRESHOLD = badness;
  }
}
@@ -3304,18 +3304,18 @@
  chosen 
-%7B%7D
+%25s
  vs firs
@@ -3320,10 +3320,10 @@
 rst 
-%7B%7D
+%25s
 %22, p
@@ -5742,8 +5742,9 @@
 s;%0A  %7D%0A%7D
+%0A

fix logging format string