package util;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;

import data.MultipleHMKeys;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import data.Settings;
import gui.GUIController;
import gui.ValidationUtil;

public class Util {
	public final static Logger logger = LogManager.getLogger(Util.class);

	/**
	 * Converts a number to a more readable format.
	 * 12345 -> 12.345
	 * 12345,678 -> 12.345,67
	 *
	 * @param o byte, double, float, int,long, short
	 *
	 * @return number formatted with thousands separator and 2 decimal places (floats)
	 */
	private static String formatNumberReadable(Object o) {
		if (isInstanceOfInteger(o))
			return String.format("%,d", o);
		else if (isInstanceOfFloat(o))
			return String.format("%,.2f", o);
		else
			return "- invalid input format -";
	}

	public static String formatNumberAsPercent(Object o, String punctuation) {
	    if(punctuation.equals("punctuation.COMMA")) {
            return MessageFormat.format("{0,number,#.### %}", o).replace('.', ',');
        } else {
            return MessageFormat.format("{0,number,#.### %}", o);
        }
	}

	public static String formatNumberForExport(Object o, String punctuation) {
        if(punctuation.equals("punctuation.COMMA")) {
            return MessageFormat.format("{0,number,#.##}", o).replace('.', ',');
        } else {
            return MessageFormat.format("{0,number,#.##}", o);
        }

	}

    public static String formatNumberForLongExport(Object o, String punctuation) {
        if(punctuation.equals("punctuation.COMMA")) {
            return MessageFormat.format("{0,number,#.########}", o).replace('.', ',');
        } else {
            return MessageFormat.format("{0,number,#.########}", o);
        }
    }

	private static boolean isInstanceOfInteger(Object o) {
		Set<Class<?>> types = new HashSet<>();
		types.add(Byte.class);
		types.add(Short.class);
		types.add(Integer.class);
		types.add(Long.class);

		return types.contains(o.getClass());
	}

	private static boolean isInstanceOfFloat(Object o) {
		Set<Class<?>> types = new HashSet<>();
		types.add(Float.class);
		types.add(Double.class);

		return types.contains(o.getClass());
	}

	/**
	 * Generic map converter -> since AtomicLongs aren't as comparable.
	 * Converts ConcurrentHashMap<K, AtomicLong> to HashMap<K, Long>
	 */
	public static <K, V> Map<MultipleHMKeys, Long> atomicInt2StringAndInt(Map<K, V> map) {
		Map m = new HashMap<MultipleHMKeys, Long>();

		for (Map.Entry<K, V> e : map.entrySet()) {
			m.put(e.getKey(), ((AtomicLong) e.getValue()).longValue());
		}

		return m;
	}


	/**
	 * Sorts a map in a descending order by value.
	 */
	public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map, int limit) {
		/*
		sorted() in itself is O(1), since it's an intermediate operation that
		doesn't consume the stream, but simply adds an operation to the pipeline.
		Once the stream is consumed by a terminal operation, the sort happens and
		 either
		- it doesn't do anything (O(1)) because the stream knows that the
		elements are already sorted (because they come from a SortedSet, for example)
		- or the stream is not parallel, and it delegates to Arrays.sort() (O(n log n))
		- or the stream is parallel, and it delegates to Arrays.parallelSort() (O(n log n))

		As of JDK 8, the main sorting algorithm which is also used in standard
		stream API implementation for sequential sorting is TimSort. Its worst
		case is O(n log n), but it works incredibly fast (with O(n) and quite
		small constant) if data is presorted (in forward or reverse direction)
		or partially presorted (for example, if you concatenate two sorted lists
		and sort them again).
		*/
		// if limit is set to 0 or less, we take that to mean no limit at all
		if (limit <= 0) {
			limit = map.size();
		}

		TimeWatch watch = TimeWatch.start();

		// sort by alphabet
		Map<K, V> alphaSorted = new LinkedHashMap<>();
		map.entrySet().stream().sorted((a, b) -> ((MultipleHMKeys)a.getKey()).compareTo((MultipleHMKeys)b.getKey())).limit(limit)
				.forEachOrdered(e -> alphaSorted.put(e.getKey(), e.getValue()));



		Map<K, V> result = new LinkedHashMap<>();
		alphaSorted.entrySet().stream().sorted((a, b) -> (int) ((Long) b.getValue() - (Long) a.getValue())).limit(limit)
				.forEachOrdered(e -> result.put(e.getKey(), e.getValue()));

		if (Settings.PRINT_LOG) {
			System.out.println(String.format("Elapsed time for sorting %s items: %s",
					formatNumberReadable(result.size()),
					watch.toFullTime()));
		}

		return result;
	}


	/**
	 * Used for passing optional integer values for sorting.
	 */
	public static int getValidInt(int... i) {
		if (i == null || i.length < 1 || i[0] <= 0) {
			return 0;
		} else {
			return i[0];
		}
	}

	/**
	 * Check whether a map is empty. It also considers an edge case where map's keys are lists to check if those lists are empty.
	 */
	public static <K, V> boolean isMapEmpty(Map<K, V> map) {
		if (map.isEmpty()) {
			// default
			return true;
		}

		// otherwise check if keys map to values that are empty
		for (V v : map.values()) {
			// todo: generalize to all collections if/when needed
			ArrayList<String> vl = new ArrayList((List<String>) v);
			if (!vl.isEmpty()) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Returns the location of the main class if possible, otherwise null
	 */
	public static File getWorkingDirectory() {
		// get location of the currently executing class
		String path = GUIController.class.getProtectionDomain().getCodeSource().getLocation().getPath();

		logger.info("working dir path: ", path);

		String decodedPath = null;
		try {
			decodedPath = URLDecoder.decode(path, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			logger.error("decoding: ", e);
			// e.printStackTrace();
		}

		if (decodedPath != null) {
			File workingDirectory = new File(decodedPath);

			// in case it's a file (class is packaged inside a jar), select its parent folder
			workingDirectory = workingDirectory.isFile() ? workingDirectory.getParentFile() : workingDirectory;

			if (ValidationUtil.isReadableDirectory(workingDirectory)) {
				logger.info("working dir is ok: ", workingDirectory.getAbsolutePath());
				return workingDirectory;
			}
		}

		logger.info("working dir returing null");
		return null;
	}
}
