package caevo.util;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.ObjectInputStream;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;

import caevo.Timex;
import edu.stanford.nlp.classify.Classifier;
import edu.stanford.nlp.io.IOUtils;
import edu.stanford.nlp.stats.Counter;

public class Util {
  public Util() {
  }

  public static void force24hrTimex(Timex timex) {
    String value = timex.getValue();
    if (!timex.isReference()) {
      if (value.matches("^\\d\\d\\d\\d-\\d\\d-\\d\\d.+")) {
        System.out.println(
            "Changing timex " + value + " to " + value.substring(0, 10));
        timex.setValue(value.substring(0, 10));
      }
    }
  }

  /**
   * Read a single serialized classifier into memory.
   * 
   * @param url
   *          The path to the model.
   * @return The classifier object.
   */
  public static Classifier<String, String> readClassifierFromFile(URL url) {
    if (url == null)
      System.out.println("ERROR: null classifier path!");
    else {
      try {
        ObjectInputStream ois = new ObjectInputStream(
            new BufferedInputStream(new GZIPInputStream(url.openStream())));
        Object o = ois.readObject();
        ois.close();
        Classifier<String, String> classifier = (Classifier<String, String>) o;
        return classifier;
      } catch (Exception ex) {
        System.out.println("ERROR: Had fatal trouble loading url=" + url);
        ex.printStackTrace();
        System.exit(1);
      }
    }
    return null;
  }

  public static Classifier<String, String> readClassifierFromFile(String path) {
    try {
      Classifier<String, String> classifier = (Classifier<String, String>) IOUtils
          .readObjectFromFile(path);
      return classifier;
    } catch (Exception ex) {
      System.out.println("ERROR: Had fatal trouble loading path=" + path);
      ex.printStackTrace();
      System.exit(1);
    }
    return null;
  }

  /**
   * Given an event, increment its count by the given amount.
   */
  public static <E> void incrementCount(Map<E, Integer> counts, E key,
      int count) {
    Integer currentCount = counts.get(key);
    if (currentCount == null)
      currentCount = 0;
    counts.put(key, currentCount + count);
  }

  /**
   * Given an event, increment its count by the given amount.
   */
  public static void incrementCount(Map<String, Float> counts,
      String eventString, float count) {
    Float currentCount = counts.get(eventString);
    if (currentCount == null)
      currentCount = 0.0f;
    counts.put(eventString, currentCount + count);
  }

  /**
   * Given an event, increment its count by the given amount.
   */
  public static void incrementCount(Map<String, Double> counts,
      String eventString, double count) {
    Double currentCount = counts.get(eventString);
    if (currentCount == null)
      currentCount = 0.0;
    counts.put(eventString, currentCount + count);
  }

  /**
   * Given an Integer ID, increment its count by the given amount.
   */
  public static void incrementCount(Map<Integer, Integer> counts, Integer id,
      int count) {
    Integer currentCount = counts.get(id);
    if (currentCount == null)
      currentCount = 0;
    counts.put(id, currentCount + count);
  }

  /**
   * Given an Integer ID, increment its count by the given amount.
   */
  public static void incrementCount(Map<Integer, Float> counts, Integer id,
      float count) {
    Float currentCount = counts.get(id);
    if (currentCount == null)
      currentCount = 0.0f;
    counts.put(id, currentCount + count);
  }

  /**
   * Given an Integer ID, increment its count by the given amount.
   */
  public static void incrementCount(Map<Integer, Double> counts, Integer id,
      double count) {
    Double currentCount = counts.get(id);
    if (currentCount == null)
      currentCount = 0.0;
    counts.put(id, currentCount + count);
  }

  public static int sumValues(Map<String, Integer> counts) {
    int sum = 0;
    if (counts != null)
      for (Map.Entry<String, Integer> entry : counts.entrySet())
        sum += entry.getValue();
    return sum;
  }

  public static <E> void countsToFile(String path, Map<E, Integer> counts) {
    try {
      PrintWriter writer;
      writer = new PrintWriter(new BufferedWriter(new FileWriter(path, false)));
      for (E key : sortKeysByIntValues(counts)) {
        writer.write(key + "\t" + counts.get(key) + "\n");
      }
      writer.close();
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  public static Map<String, Integer> countsFromFile(String path) {
    Map<String, Integer> counts = new HashMap<String, Integer>();
    try {
      BufferedReader in = new BufferedReader(new FileReader(path));
      String line;
      while ((line = in.readLine()) != null) {
        int tabindex = line.indexOf('\t');
        String key = line.substring(0, tabindex);
        int count = Integer.valueOf(line.substring(tabindex + 1));
        counts.put(key, count);
      }
      in.close();
    } catch (Exception ex) {
      System.err.println("Error opening " + path);
      ex.printStackTrace();
    }
    return counts;
  }

  public static List<String> readLinesFromFile(String path) {
    List<String> lines = new ArrayList<String>();
    try {
      BufferedReader in = new BufferedReader(new FileReader(path));
      String line;
      while ((line = in.readLine()) != null) {
        lines.add(line);
      }
      in.close();
    } catch (Exception ex) {
      System.err.println("Error reading from " + path);
      ex.printStackTrace();
    }
    return lines;
  }

  /**
   * Read a file of strings and counts. Return all strings that have a count
   * greater than or equal to the given threshold n.
   * 
   * @param path
   *          The file path.
   * @param n
   *          The threshold, all counts higher or equal are returned.
   * @return All keys in the file that have higher/equal counts to n.
   */
  public static Set<String> keysFromFileMeetingThreshold(String path, int n) {
    Set<String> keys = new HashSet<String>();
    try {
      BufferedReader in = new BufferedReader(new FileReader(path));
      String line;
      while ((line = in.readLine()) != null) {
        int tabindex = line.indexOf('\t');
        String key = line.substring(0, tabindex);
        int count = Integer.valueOf(line.substring(tabindex + 1));
        if (count >= n)
          keys.add(key);
      }
      in.close();
    } catch (Exception ex) {
      System.err.println("Error opening " + path);
      ex.printStackTrace();
    }
    return keys;
  }

  /**
   * Scale the values of the map to be between [0,1].
   */
  public static void scaleToUnit(Map<Object, Double> counts) {
    double max = 0.0;
    double min = Double.MAX_VALUE;
    for (Map.Entry<Object, Double> entry : counts.entrySet()) {
      double value = entry.getValue();
      if (value > max)
        max = value;
      if (value < min)
        min = value;
    }

    double range = max - min;

    // Scale between [0,1]
    for (Map.Entry<Object, Double> entry : counts.entrySet()) {
      double value = entry.getValue();
      double scaledValue = (value - min) / range;
      entry.setValue(scaledValue);
    }
  }

  /**
   * Scale the values of the map to be between [0,1].
   */
  public static void scaleToUnit(SortableScore scores[]) {
    double max = 0.0;
    double min = Double.MAX_VALUE;
    for (SortableScore score : scores) {
      double value = score.score();
      if (value > max)
        max = value;
      if (value < min)
        min = value;
    }

    double range = max - min;

    // Scale between [0,1]
    for (SortableScore score : scores) {
      double value = score.score();
      double scaledValue = (value - min) / range;
      score.setScore(scaledValue);
    }
  }

  public static <E> void scaleToUnit(SortableObject<E> scores[]) {
    double max = 0.0;
    double min = Double.MAX_VALUE;
    for (SortableObject<E> score : scores) {
      double value = score.score();
      if (value > max)
        max = value;
      if (value < min)
        min = value;
    }

    double range = max - min;

    // Scale between [0,1]
    for (SortableObject<E> score : scores) {
      double value = score.score();
      double scaledValue = (value - min) / range;
      score.setScore(scaledValue);
    }
  }

  /**
   * Read integers from a file, one integer per line.
   */
  public static List<Integer> slurpIntegers(String filename) {
    List<Integer> guesses = new ArrayList<Integer>();

    try {
      BufferedReader in = new BufferedReader(new FileReader(filename));
      String line;

      while ((line = in.readLine()) != null) {
        if (line.matches("-?\\d+"))
          guesses.add(Integer.parseInt(line));
        else
          System.out
              .println("WARNING: skipping line in " + filename + ": " + line);
      }

      in.close();
    } catch (Exception ex) {
      ex.printStackTrace();
    }

    return guesses;
  }

  /**
   * @return A String array of length two, the directory and the file. Directory
   *         is null if there is none.
   */
  public static String[] splitDirectoryAndFile(String path) {
    int index = path.lastIndexOf(File.separatorChar);
    String[] pair = new String[2];

    // strip trailing separators
    while (index >= 0 && index == path.length() - 1) {
      path = path.substring(0, path.length() - 1);
      index = path.lastIndexOf(File.separatorChar);
    }

    // if no directory in the path
    if (index == -1) {
      pair[0] = null;
      pair[1] = path;
    }
    // split off the directory
    else {
      pair[0] = path.substring(0, index);
      pair[1] = path.substring(index + 1);
    }
    return pair;
  }

  /**
   * @return A new list with only n elements.
   */
  public static <E> List<E> getFirstN(Collection<E> objects, int n) {
    if (objects == null)
      return null;

    List<E> newlist = new ArrayList<E>();
    int i = 0;
    for (E item : objects) {
      if (i < n)
        newlist.add(item);
      else
        break;
      i++;
    }
    return newlist;
  }

  /**
   * DESTRUCTIVE: Trim the list to just the first n objects.
   */
  public static <E> void firstN(List<E> objects, int n) {
    if (objects == null)
      return;

    while (objects.size() > n) {
      objects.remove(objects.size() - 1);
    }
  }

  /**
   * Create a new array with just the first n objects.
   */
  public static SortableScore[] firstN(SortableScore[] scores, int n) {
    if (scores == null)
      return null;

    int newsize = Math.min(n, scores.length);
    SortableScore[] arr = new SortableScore[newsize];
    for (int i = 0; i < newsize; i++)
      arr[i] = scores[i];
    return arr;
  }

  public static <E> List<E> mergeLists(List<E> first, List<E> second) {
    List<E> merged = new ArrayList<E>(first);
    for (E obj : second) {
      if (!merged.contains(obj))
        merged.add(obj);
    }
    return merged;
  }

  public static <E> List<E> arrayToList(E[] arr) {
    List<E> thelist = new ArrayList<E>();
    for (E obj : arr)
      thelist.add(obj);
    return thelist;
  }

  /**
   * String match search for a string in a list.
   */
  public static boolean findInListOfStrings(List<String> items, String target) {
    if (items == null || target == null)
      return false;

    for (String item : items)
      if (target.equalsIgnoreCase(item))
        return true;

    return false;
  }

  public static <E> List<E> collectionToList(Collection<E> theset) {
    List<E> thelist = new ArrayList<E>();
    for (E obj : theset)
      thelist.add(obj);
    return thelist;
  }

  public static String[] sortStrings(Collection<String> unsorted) {
    String[] sorted = new String[unsorted.size()];
    sorted = unsorted.toArray(sorted);
    Arrays.sort(sorted);
    return sorted;
  }

  /**
   * Sort the keys of the map by their values, return the list in order.
   */
  public static <E> List<E> sortCounterKeys(Counter<E> map) {
    return sortCounterKeys(map, false);
  }

  public static <E> List<E> sortCounterKeys(Counter<E> map, boolean reverse) {
    SortableObject<E>[] sorted = new SortableObject[map.size()];
    int i = 0;
    for (Map.Entry<E, Double> entry : map.entrySet())
      sorted[i++] = new SortableObject<E>(entry.getValue(), entry.getKey());
    if (!reverse)
      Arrays.sort(sorted);
    else
      Arrays.sort(sorted, Collections.reverseOrder());

    List<E> strs = new ArrayList<E>(sorted.length);
    for (SortableObject<E> obj : sorted)
      strs.add(obj.key());
    return strs;
  }

  /**
   * Sort the keys of the map by their values, return the list in order.
   */
  public static <E> List<E> sortKeysByValues(Map<E, Double> map) {
    return sortKeysByValues(map, false);
  }

  public static <E> List<E> sortKeysByValues(Map<E, Double> map,
      boolean reverse) {
    SortableObject<E>[] sorted = new SortableObject[map.size()];
    int i = 0;
    for (Map.Entry<E, Double> entry : map.entrySet())
      sorted[i++] = new SortableObject<E>(entry.getValue(), entry.getKey());
    if (!reverse)
      Arrays.sort(sorted);
    else
      Arrays.sort(sorted, Collections.reverseOrder());

    List<E> strs = new ArrayList<E>(sorted.length);
    for (SortableObject<E> obj : sorted)
      strs.add(obj.key());
    return strs;
  }

  /**
   * Sort the keys of the map by their values, return the list in order.
   */
  public static <E> List<E> sortKeysByIntValues(Map<E, Integer> map) {
    return sortKeysByIntValues(map, false);
  }

  public static <E> List<E> sortKeysByIntValues(Map<E, Integer> map,
      boolean reverse) {
    if (map == null)
      return null;
    SortableObject<E>[] sorted = new SortableObject[map.size()];
    int i = 0;
    for (Map.Entry<E, Integer> entry : map.entrySet())
      sorted[i++] = new SortableObject<E>(entry.getValue(), entry.getKey());
    if (!reverse)
      Arrays.sort(sorted);
    else
      Arrays.sort(sorted, Collections.reverseOrder());

    List<E> strs = new ArrayList<E>(sorted.length);
    for (SortableObject<E> obj : sorted)
      strs.add((E) obj.key());
    return strs;
  }

  public static void printMapSortedByValue(Map<String, Integer> map) {
    printMapSortedByValue(map, (map == null ? 0 : map.size()));
  }

  public static void printMapSortedByValue(Map<String, Integer> map, int max) {
    if (map == null || map.size() == 0)
      System.out.println("null");
    else {
      SortableObject<String>[] sorted = new SortableObject[map.size()];
      int i = 0;
      for (Map.Entry<String, Integer> entry : map.entrySet())
        sorted[i++] = new SortableObject<String>(entry.getValue(),
            entry.getKey());
      Arrays.sort(sorted);

      int num = 0;
      for (SortableObject<String> obj : sorted) {
        System.out.print(obj.key + " " + map.get(obj.key) + " ");
        num++;
        if (num == max)
          break;
      }
      System.out.println();
    }
  }

  public static void printDoubleMapSortedByValue(Map<String, Double> map,
      int max) {
    if (map == null || map.size() == 0)
      System.out.println("null");
    else {
      SortableObject<String>[] sorted = new SortableObject[map.size()];
      int i = 0;
      for (Map.Entry<String, Double> entry : map.entrySet())
        sorted[i++] = new SortableObject<String>(entry.getValue(),
            entry.getKey());
      Arrays.sort(sorted);

      int num = 0;
      for (SortableObject<String> obj : sorted) {
        System.out.printf("%s %.3f ", obj.key, map.get(obj.key));
        num++;
        if (num == max)
          break;
      }
      System.out.println();
    }
  }

  /**
   * Removes all map entries whose integer count is the cutoff or less.
   */
  public static void trimIntegerCounts(Map<String, Integer> map, int cutoff) {
    List<String> removed = new ArrayList<String>();

    for (Map.Entry<String, Integer> entry : map.entrySet()) {
      Integer count = entry.getValue();
      // Remove the pair if it is sparse
      if (count == null || count <= cutoff)
        removed.add(entry.getKey());
    }

    for (String key : removed)
      map.remove(key);
    // System.out.println("Trimmed " + removed.size() + " pairs");
  }

  /**
   * Removes all map entries whose integer count is the cutoff or less.
   */
  public static void trimFloatCounts(Map<String, Float> map, float cutoff) {
    List<String> removed = new ArrayList<String>();

    for (Map.Entry<String, Float> entry : map.entrySet()) {
      Float count = entry.getValue();
      // Remove the pair if it is sparse
      if (count == null || count <= cutoff)
        removed.add(entry.getKey());
    }

    for (String key : removed)
      map.remove(key);
    // System.out.println("Trimmed " + removed.size() + " pairs");
  }

  /**
   * Create a string list of the items, up to n long.
   */
  public static String collectionToString(Collection<String> set, int n) {
    if (set == null)
      return "";

    StringBuffer buf = new StringBuffer();
    int i = 0;
    for (String str : set) {
      if (i > 0)
        buf.append(' ');
      buf.append(str);
      i++;
      if (i == n)
        break;
    }
    return buf.toString();
  }

  public static double calculateEntropy(double[] probs) {
    double entropy = 0.0;
    for (int i = 0; i < probs.length; i++) {
      if (probs[i] > 0.0)
        entropy -= probs[i] * Math.log(probs[i]);
    }
    return entropy;
  }

  /**
   * Standard factorial calculation.
   */
  public static int factorial(int n) {
    if (n == 0)
      return 1;
    int fact = 1;
    for (int i = 1; i <= n; i++)
      fact *= i;
    return fact;
  }

  /**
   * Standard "n choose k" calculation.
   */
  public static int choose(int n, int k) {
    return factorial(n) / factorial(k) * factorial(n - k);
  }

  public static void reportMemory() {
    Runtime runtime = Runtime.getRuntime();
    long mb = 1024 * 1024;
    // long max = runtime.maxMemory();
    long used = runtime.totalMemory() - runtime.freeMemory();
    System.out.printf(
        "......................... memory in use: %d MB .........................%n",
        used / mb);
  }

  public static void reportElapsedTime(long startTime) {
    String str = timeString(System.currentTimeMillis() - startTime);
    System.out.println("............................ runtime " + str
        + " ..........................");
  }

  public static String timeString(long milli) {
    long seconds = milli / 1000;
    long minutes = seconds / 60;
    long hours = minutes / 60;
    minutes = minutes % 60;
    seconds = seconds % 60;
    return (hours + "h " + minutes + "m " + seconds + "s");
  }

  public static void main(String[] args) {
    double[] probs = new double[4];
    probs[0] = 0.7;
    probs[1] = 0.3;
    probs[2] = 0.45;
    probs[3] = 0.1;

    System.out.println("sorted = " + Arrays.toString(probs));

    String feature = "running somewhere";
    System.out.println("bytes = " + feature.getBytes().length);

    // System.out.println("entropy = " + calculateEntropy(probs));
  }
}
