package zemberek.core.collections;

import com.google.common.collect.Lists;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * A simple set like data structure for counting unique elements. Not thread safe.
 */
public class Histogram<T> implements Iterable<T> {

  private final UIntValueMap<T> map;

  public Histogram(int initialSize) {
    map = new UIntValueMap<>(initialSize);
  }

  public Histogram(Map<T, Integer> countMap) {
    this.map = new UIntValueMap<>(countMap.size());
    for (T t : countMap.keySet()) {
      this.map.put(t, countMap.get(t));
    }
  }

  public Histogram() {
    map = new UIntValueMap<>();
  }

  /**
   * Loads from file with format: [key][delimiter][count]
   */
  public static Histogram<String> loadFromLines(List<String> lines, char delimiter) {
    return loadFromLines(lines, delimiter, true);
  }

  public static Histogram<String> loadFromLines(
      List<String> lines,
      char delimiter,
      boolean keyComesFirst) {
    Histogram<String> result = new Histogram<>(lines.size());
    for (String s : lines) {
      int index = s.indexOf(delimiter);
      if (index <= 0) {
        throw new IllegalStateException("Bad histogram line = " + s);
      }
      String item = keyComesFirst ? s.substring(0, index) : s.substring(index + 1);
      String countStr = keyComesFirst ? s.substring(index + 1) : s.substring(0, index);
      int count = Integer.parseInt(countStr);
      result.add(item, count);
    }
    return result;
  }

  /**
   * Loads a String Histogram from a file. Counts are supposedly delimited with `delimiter`
   * character. format: [key][delimiter][count]
   *
   * @param path file path
   * @param delimiter delimiter
   * @return a Histogram.
   */
  public static Histogram<String> loadFromUtf8File(Path path, char delimiter) throws IOException {
    List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
    return loadFromLines(lines, delimiter);
  }

  public static Histogram<String> loadFromUtf8File(
      Path path,
      char delimiter,
      boolean keyComesFirst) throws IOException {
    List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
    return loadFromLines(lines, delimiter, keyComesFirst);
  }


  public static void serializeStringHistogram(Histogram<String> h, DataOutputStream dos)
      throws IOException {
    dos.writeInt(h.size());
    for (String key : h.map) {
      dos.writeUTF(key);
      dos.writeInt(h.getCount(key));
    }
  }

  public static Histogram<String> deserializeStringHistogram(DataInputStream dis)
      throws IOException {
    int size = dis.readInt();
    if (size < 0) {
      throw new IllegalStateException(
          "Cannot deserialize String histogram. Count value is negative : " + size);
    }
    Histogram<String> result = new Histogram<>(size);
    for (int i = 0; i < size; i++) {
      result.set(dis.readUTF(), dis.readInt());
    }
    return result;
  }

  public void saveSortedByCounts(Path path, String delimiter)
      throws IOException {
    try (PrintWriter pw = new PrintWriter(path.toFile(), StandardCharsets.UTF_8.name())) {
      List<T> sorted = getSortedList();
      for (T t : sorted) {
        pw.println(t + delimiter + getCount(t));
      }
    }
  }

  public void saveSortedByKeys(Path path, String delimiter, Comparator<T> comparator)
      throws IOException {
    try (PrintWriter pw = new PrintWriter(path.toFile(), StandardCharsets.UTF_8.name())) {
      List<T> sorted = getSortedList(comparator);
      for (T t : sorted) {
        pw.println(t + delimiter + getCount(t));
      }
    }
  }

  /**
   * adds an element. and increments it's count.
   *
   * @param t element to add.
   * @return the count of the added element.
   * @throws NullPointerException if element is null.
   */
  public int add(T t) {
    return add(t, 1);
  }

  /**
   * adds an element. and increments it's count.
   *
   * @param t element to add.
   * @param count the count of the element to add.
   * @return the count of the added element.
   * @throws NullPointerException if element is null.
   */
  public int add(T t, int count) {
    return map.incrementByAmount(t, count);
  }

  /**
   * merges another Histogram to this one.
   *
   * @param otherSet another Histogram
   */
  public void add(Histogram<T> otherSet) {
    if (otherSet == null) {
      throw new NullPointerException("Histogram cannot be null");
    }
    for (T t : otherSet) {
      add(t, otherSet.getCount(t));
    }
  }

  public T lookup(T item) {
    return map.lookup(item);
  }

  /**
   * adds a collection of elements.
   *
   * @param collection a collection of elements.
   */
  public void add(Collection<T> collection) {

    if (collection == null) {
      throw new NullPointerException("collection cannot be null");
    }
    collection.forEach(this::add);
  }

  /**
   * adds an array of elements.
   *
   * @param array an array of elements to add.
   */
  @SafeVarargs
  public final void add(T... array) {
    if (array == null) {
      throw new NullPointerException("array cannot be null");
    }
    for (T t : array) {
      add(t);
    }
  }

  /**
   * returns the total element count of the counting set.
   *
   * @return element count.
   */
  public int size() {
    return map.size();
  }

  /**
   * inserts the element and its value. it overrides the current count
   *
   * @param t element
   * @param c count value which will override the current count value.
   */
  public void set(T t, int c) {
    map.put(t, c);
  }

  public int decrementIfPositive(T t) {
    if (t == null) {
      throw new NullPointerException("Element cannot be null");
    }
    int c = map.get(t);
    if (c > 0) {
      return map.decrement(t);
    } else {
      return 0;
    }
  }

  /**
   * current count of the given element
   *
   * @param t element
   * @return count of the element. if element does not exist, 0
   */
  public int getCount(T t) {
    int i = map.get(t);
    return i < 0 ? 0 : i;
  }


  /**
   * if element exist.
   *
   * @param t element.
   * @return if element exists.
   */
  public boolean contains(T t) {
    return map.contains(t);
  }

  /**
   * returns the Elements in a list sorted by count, descending..
   *
   * @return Elements in a list sorted by count, descending..
   */
  public List<T> getTop(int n) {
    if (n > size()) {
      n = size();
    }
    List<IntValueMap.Entry<T>> l = map.getAsEntryList();
    Collections.sort(l);
    List<T> result = l.stream().map(e -> e.key).collect(Collectors.toList());
    return Lists.newArrayList(result.subList(0, n));
  }

  /**
   * removes the items that has a count smaller than minCount
   *
   * @param minCount minimum count amount to remain in the set.
   * @return reduced set.
   */
  public int removeSmaller(int minCount) {
    Set<T> toRemove = new HashSet<>();
    int removeCount = 0;
    for (T key : map) {
      if (map.get(key) < minCount) {
        toRemove.add(key);
        removeCount++;
      }
    }
    toRemove.forEach(map::remove);
    return removeCount;
  }


  /**
   * removes the items that has a count larger than minCount
   *
   * @param maxCount maximum count amount to remain in the set.
   * @return reduced set.
   */
  public int removeLarger(int maxCount) {
    Set<T> toRemove = new HashSet<>();
    int removeCount = 0;
    for (T key : map) {
      if (map.get(key) > maxCount) {
        toRemove.add(key);
        removeCount++;
      }
    }
    toRemove.forEach(map::remove);
    return removeCount;
  }

  /**
   * counts the items those count is smaller than amount
   *
   * @param amount to check size
   * @return count.
   */
  public int sizeSmaller(int amount) {
    return map.sizeSmaller(amount);
  }

  /**
   * removes an item.
   *
   * @param t item to removed.
   */
  public void remove(T t) {
    map.remove(t);
  }

  /**
   * removes all items.
   *
   * @param items item to removed.
   */
  public void removeAll(Iterable<T> items) {
    for (T t : items) {
      map.remove(t);
    }
  }

  /**
   * counts the items those count is smaller than amount
   *
   * @param amount amount to check size
   * @return count.
   */
  public int sizeLarger(int amount) {
    return map.sizeLarger(amount);
  }

  /**
   * total count of items those value is between "minValue" and "maxValue"
   *
   * @param minValue minValue inclusive
   * @param maxValue maxValue inclusive
   * @return total count of items those value is between "minValue" and "maxValue"
   */
  public long totalCount(int minValue, int maxValue) {
    return map.sumOfValues(minValue, maxValue);
  }

  /**
   * returns the max count value.
   *
   * @return the max value in the set if set is empty, 0 is returned.
   */
  public int maxValue() {
    return map.maxValue();
  }

  /**
   * returns the min count value.
   *
   * @return the min value in the set, if set is empty, Integer.MAX_VALUE is returned.
   */
  public int minValue() {
    return map.minValue();
  }

  /**
   * returns the list of elements whose count is equal to "value"
   *
   * @param value the value for the keys
   * @return the list of elements whose count is equal to "value"
   */
  public List<T> getItemsWithCount(int value) {
    List<T> keys = new ArrayList<>();
    for (T key : map) {
      if (map.get(key) == value) {
        keys.add(key);
      }
    }
    return keys;
  }

  /**
   * Returns the list of elements whose count is between "min" and "max" (both inclusive)
   *
   * @param min min
   * @param max max
   * @return Returns the list of elements whose count is between "min" and "max" (both inclusive)
   */
  public List<T> getItemsWithCount(int min, int max) {
    List<T> keys = new ArrayList<>();
    for (T key : map) {
      int value = map.get(key);
      if (value >= min && value <= max) {
        keys.add(key);
      }
    }
    return keys;
  }

  /**
   * Percentage of total count of [min-max] to total counts.
   *
   * @param min min (inclusive)
   * @param max max (inclusive)
   * @return count.
   */
  public double countPercent(int min, int max) {
    return (totalCount(min, max) * 100d) / totalCount();
  }

  /**
   * returns the Elements in a list sorted by count, descending.
   *
   * @return Elements in a list sorted by count, descending.
   */
  public List<T> getSortedList() {
    List<T> l = Lists.newArrayListWithCapacity(map.size());
    l.addAll(getSortedEntryList().stream().map(entry -> entry.key).collect(Collectors.toList()));
    return l;
  }

  /**
   * Returns keys that both histogram contain.
   * @param other Another Histogram
   * @return A set of keys that both histogram contain.
   */
  public Set<T> getIntersectionOfKeys(Histogram<T> other) {
    LinkedHashSet<T> result = new LinkedHashSet<>();
    Histogram<T> smaller = other.size() < size() ? other : this;
    Histogram<T> larger = smaller == this ? other : this;
    for (T t : smaller.getSortedList()) {
      if (larger.contains(t)) {
        result.add(t);
      }
    }
    return result;
  }

  /**
   * returns the Elements in a list sorted by count, descending..
   *
   * @return Elements in a list sorted by count, descending..
   */
  public List<IntValueMap.Entry<T>> getSortedEntryList() {
    List<IntValueMap.Entry<T>> l = map.getAsEntryList();
    Collections.sort(l);
    return l;
  }

  /**
   * returns the Elements in a list sorted by count, descending..
   *
   * @return Elements in a list sorted by count, descending..
   */
  public List<IntValueMap.Entry<T>> getEntryList() {
    return map.getAsEntryList();
  }


  /**
   * @return total count of the items in the input Iterable.
   */
  public long totalCount(Iterable<T> it) {
    long count = 0;
    for (T t : it) {
      count += getCount(t);
    }
    return count;
  }

  /**
   * returns the Elements in a list sorted by the given comparator..
   *
   * @param comp a Comarator of T
   * @return Elements in a list sorted by the given comparator..
   */
  public List<T> getSortedList(Comparator<T> comp) {
    List<T> l = Lists.newArrayList(map);
    l.sort(comp);
    return l;
  }

  /**
   * returns elements in a set.
   *
   * @return a set containing the elements.
   */
  public Set<T> getKeySet() {
    return map.getKeySet();
  }


  /**
   * returns an iterator for elements.
   *
   * @return returns an iterator for elements.
   */
  public Iterator<T> iterator() {
    return map.iterator();
  }

  /**
   * Sums all item's counts.
   *
   * @return sum of all item's count.
   */
  public long totalCount() {
    return map.sumOfValues();
  }

  private class CountComparator implements Comparator<Map.Entry<T, Integer>> {

    public int compare(Map.Entry<T, Integer> o1, Map.Entry<T, Integer> o2) {
      return (o2.getValue() < o1.getValue()) ? -1 : ((o2.getValue() > o1.getValue()) ? 1 : 0);
    }
  }
}