package math.utils;

import java.awt.*;
import java.io.*;
import java.util.*;

/**
 * Klasa oblicza czestość występowania elementu typu T *
 * @param <T> obiekt dowolnej klasy
 */
public class FrequencyMap<T> {
	private TreeMap<T, Integer> map;
	private PrintWriter pw;
	private int elementy;

	public FrequencyMap(){
		map = new TreeMap<>();
	}

	public Point[] getAllPoints() {
		Set<Map.Entry<T, Integer>> set = map.entrySet();
		int size = set.size();
		Point[] points = new Point[size];
		int i = 0;
		for(Map.Entry<T, Integer> entry : set){
			points[i] = new Point((Integer) entry.getKey(),
					entry.getValue());
			i++;
		}
		return points;
	}

	/**
	 * Dodaje obiekt do kolekcji, a jeśli element już w niej jest
	 * dodaje wystąpienie danego elementu.
	 * @param obj - dodawany element
	 */
	public void add(T obj) {
		Integer count = map.get(obj);
		if(count == null){
			map.put(obj, 1);
		}
		else{
			map.put(obj, ++count);
		}
		elementy++;
	}

	/**
	 * Usuwa wystąpienie danego elementu z kolekcji. Jeśli pozostał
	 * tylko jeden element usuwa go z kolekcji
	 * @param obj - usuwany obiekt
	 */
	public void remove(T obj) {
		Integer count = map.get(obj);
		if(count != null){
			if(count == 1){
				map.remove(obj);
			}
			else{
				map.put(obj, --count);
			}
		}
		elementy--;
	}

	/**
	 * drukuje liczbę wystąpień (częstość) danego obiektu
	 * na konsoli 
	 * @param obj - obiekt, którego liczbę wystąpień chcemy poznać
	 */
	public void print(T obj) {
		if(map.containsKey(obj)){
			Set<Map.Entry<T, Integer>> set = map.entrySet();
			for(Map.Entry<T, Integer> entry : set){
				if((entry.getKey()).equals(obj)){
					System.out.println(entry.getKey() + " " + entry.getValue());
					break;
				}
			}
		}
	}
	public int[] getAllAsInts() {
		Point[] points = getAllPoints();
		int sum = 0;
		int counter = 0;
		for (Point point1 : points) {
			sum += point1.getY();
		}
		int[] liczby = new int[sum];
		for (Point point : points) {
			int licznik = (int) point.getY();
			int liczba = (int) point.getX();
			for (int m = 0; m < licznik; m++) {
				liczby[counter] = liczba;
				counter++;
			}
		}
		return liczby;
	}
	public long[] getAllAsLongs() {
		Tuple2L[] points = getAllAsTuplesL();
		int sum = 0;
		int counter = 0;
		for (Tuple2L point1 : points) {
			sum += point1.getY();
		}
		long[] liczby = new long[sum];
		for (Tuple2L point : points) {
			int licznik = (int) point.getY();
			int liczba = (int) point.getX();
			for (int m = 0; m < licznik; m++) {
				liczby[counter] = liczba;
				counter++;
			}
		}
		return liczby;
	}

	public Tuple2L[] getAllAsTuplesL() {
		Set<Map.Entry<T, Integer>> set = map.entrySet();
		int size = set.size();
		Tuple2L[] points = new Tuple2L[size];
		int i = 0;
		for(Map.Entry<T, Integer> entry : set){
			points[i] = new Tuple2L((Long)entry.getKey(), entry.getValue());
			i++;
		}
		return points;
	}

	public Tuple2d[] getAllTuplesD() {
		Set<Map.Entry<T, Integer>> set = map.entrySet();
		int size = set.size();
		Tuple2d[] points = new Tuple2d[size];
		int i = 0;
		for(Map.Entry<T, Integer> entry : set){
			points[i] = new Tuple2d((Double)entry.getKey(), entry.getValue());
			i++;
		}
		return points;
	}

	/**
	 * drukuje na konsoli obiekty i ich częstości, w takiej kolejności 
	 * w jakiej są uporządkowane w mapie
	 */
	public int[] printAll() {
		Set<Map.Entry<T, Integer>> set = map.entrySet();
		int i = 0;
		int[] tabl = new int[set.size()];
		for(Map.Entry<T, Integer> entry : set){
			System.out.println(entry.getKey() + " " + entry.getValue());
			tabl[i] = entry.getValue();
			i++;
		}
		return tabl;
	}

	/**
	 * drukuje na konsoli obiekty i ich częstości uporządkowane 
	 * według malejącej częstości.
	 */
	public void printAllSorted() {
		ArrayList<Freq<T, Integer>> al = new ArrayList<>();
		Set<Map.Entry<T, Integer>> set = map.entrySet();
		for(Map.Entry<T, Integer> entry : set){
			al.add(new Freq<>(entry.getKey(), entry.getValue()));
		}
		Collections.sort(al);
		for(Freq<T, Integer> f : al){
			System.out.println(f);
		}
	}

	/**
	 * Drukuje do wskazanego pliku obiekty i ich częstości uporządkowane 
	 * według malejącej częstości.
	 * @param plik - ścieżka do pliku.
	 */
	public void printAllSortedToFile(String plik) {
		try{
			pw = new PrintWriter(plik);
		} catch (FileNotFoundException e){
			e.printStackTrace();
		}
		ArrayList<Freq<T, Integer>> al = new ArrayList<>();
		Set<Map.Entry<T, Integer>> set = map.entrySet();
		for(Map.Entry<T, Integer> entry : set){
			al.add(new Freq<>(entry.getKey(), entry.getValue()));
		}
		Collections.sort(al);
		for(Freq<T, Integer> f : al){
			pw.println(f);
		}
		pw.close();
	}

	public void setMap(TreeMap<T, Integer> map) {
		this.map = map;
	}

	/** 
	 * @return liczbę zsumowaną liczbę elementów w kolekcji. W przypadku
	 * np. słów podaje łączną ogólną liczbę słów w kolekcji
	 */
	public int getElementy() {
		return elementy;
	}

	/**
	 * Zwraca częstość występowania obiektu obj. W przypadku słów zwraca
	 * liczbę wystąpień danego słowa
	 * @param obj - element, kórego częstość chcemy sprawdzić
	 * @return - częstość (liczba wystąpień) elementu obj
	 */
	public int getFrequency(T obj) {
		int freq = 0;
		if(map.containsKey(obj)){
			Set<Map.Entry<T, Integer>> set = map.entrySet();
			for(Map.Entry<T, Integer> entry : set){
				if((entry.getKey()).equals(obj)){
					freq = entry.getValue();
					break;
				}
			}
		}
		return freq;
	}

	public int razem() {
		int a = 0;
		Set<Map.Entry<T, Integer>> set = map.entrySet();
		for(Map.Entry<T, Integer> entry : set){
			a += entry.getValue();
		}
		return a;
	}

	/**
	 * Podaje wielkość kolekcji. W przypadku słów jest to liczba różnych
	 * słów w kolekcji
	 * @return - wielkość kolekcji
	 */
	public int getSize() {
		return map.size();
	}

	/**
	 * Getter
	 * @return - zwraca mapę
	 */
	public TreeMap<T, Integer> getMap() {
		return map;
	}

	/**
	 * Metoda pomocnicza do wczytywania tekstów i podziału na tokeny (słowa)
	 * @param file - ścieżka do pliku
	 * @return - mapę częstości słów w pliku file
	 */
	public static FrequencyMap<String> read(String file) {
		FrequencyMap<String> map = new FrequencyMap<>();
		try{
			try (BufferedReader in = new BufferedReader(
					new FileReader(new File(file)))) {
				String s;
				while ((s = in.readLine()) != null) {
					StringTokenizer st = new StringTokenizer(s, " ,;.");
					while (st.hasMoreTokens()) {
						map.add(st.nextToken());
					}
				}
			}
		} catch (IOException ioe){
			throw new RuntimeException(ioe);
		}
		return map;
	}

	public void printAll2() {
		Set<Map.Entry<T, Integer>> set = map.entrySet();
		for(Map.Entry<T, Integer> entry : set){
			System.out.println(entry.getKey() + " " + entry.getValue());
		}
	}
}

/**
 * Klasa pomocnicza pozwalająca na umieszczanie elementów
 * w arrayliście
 */
class Freq<K, V> implements Comparable<Freq<K, V>>{
	private K key;
	private V value;

	public Freq(K key, V value){
		this.key = key;
		this.value = value;
	}

	public K getKey() {
		return key;
	}

	public void setKey(K key) {
		this.key = key;
	}

	public V getValue() {
		return value;
	}

	public void setValue(V value) {
		this.value = value;
	}

	@Override
	public String toString() {
		return "[" + key + ", " + value + "]";
	}

	@Override
	public boolean equals(Object obj) {
		if(this == obj){
			return true;
		}
		if(obj == null){
			return false;
		}
		if(getClass() != obj.getClass()){
			return false;
		}
		if(!(obj instanceof Freq<?, ?>)){
			return false;
		}
		Freq<?, ?> fr = (Freq<?, ?>)obj;
		return key.equals(fr.getKey()) && value.equals(fr.getValue());
	}

	@Override
	public int hashCode() {
		return 17 * key.hashCode() + 19 * value.hashCode();
	}

	@Override
	public int compareTo(Freq<K, V> o) {
		Integer i1 = (Integer)this.getValue();
		Integer i2 = (Integer)o.getValue();
		return i2.compareTo(i1);
	}
}
