package nallar.tickthreading.minecraft.profiling;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import com.google.common.base.Functions;
import com.google.common.collect.Ordering;

import nallar.tickthreading.minecraft.commands.Command;
import nallar.tickthreading.util.MappingUtil;
import nallar.tickthreading.util.TableFormatter;
import net.minecraft.command.ICommandSender;
import net.minecraft.network.packet.Packet;
import net.minecraft.network.packet.Packet250CustomPayload;
import org.cliffc.high_scale_lib.NonBlockingHashMap;

public class PacketProfiler {
	private static boolean profiling = false;
	private static final Map<String, AtomicInteger> size = new NonBlockingHashMap<String, AtomicInteger>();
	private static final Map<String, AtomicInteger> count = new NonBlockingHashMap<String, AtomicInteger>();

	public static synchronized boolean startProfiling(final ICommandSender commandSender, final int time) {
		if (profiling) {
			Command.sendChat(commandSender, "Someone else is already profiling packets.");
			return false;
		}
		profiling = true;
		Command.sendChat(commandSender, "Profiling packets for " + time + " seconds.");
		new Thread() {
			@Override
			public void run() {
				try {
					Thread.sleep(time * 1000);
				} catch (InterruptedException ignored) {
				}
				Command.sendChat(commandSender, writeStats(new TableFormatter(commandSender)).toString());
				synchronized (PacketProfiler.class) {
					size.clear();
					count.clear();
					profiling = false;
				}
			}
		}.start();
		return true;
	}

	private static <T> List<T> sortedKeys(Map<T, ? extends Comparable<?>> map, int elements) {
		List<T> list = Ordering.natural().reverse().onResultOf(Functions.forMap(map)).immutableSortedCopy(map.keySet());
		return list.size() > elements ? list.subList(0, elements) : list;
	}

	private static TableFormatter writeStats(final TableFormatter tf) {
		return writeStats(tf, 9);
	}

	private static TableFormatter writeStats(final TableFormatter tf, int elements) {
		Map<String, Integer> count = new HashMap<String, Integer>();
		for (Map.Entry<String, AtomicInteger> entry : PacketProfiler.count.entrySet()) {
			count.put(entry.getKey(), entry.getValue().get());
		}
		Map<String, Integer> size = new HashMap<String, Integer>();
		for (Map.Entry<String, AtomicInteger> entry : PacketProfiler.size.entrySet()) {
			size.put(entry.getKey(), entry.getValue().get());
		}

		tf
				.heading("Packet")
				.heading("Count")
				.heading("Size");
		final List<String> sortedIdsByCount = sortedKeys(count, elements);
		for (String id : sortedIdsByCount) {
			tf
					.row(getName(id))
					.row(count.get(id))
					.row(humanReadableByteCount(size.get(id)));
		}
		tf.finishTable();
		tf.sb.append('\n');
		tf
				.heading("Packet")
				.heading("Count")
				.heading("Size");
		final List<String> sortedIdsBySize = sortedKeys(size, elements);
		for (String id : sortedIdsBySize) {
			tf
					.row(getName(id))
					.row(count.get(id))
					.row(humanReadableByteCount(size.get(id)));
		}
		tf.finishTable();
		return tf;
	}

	private static String getName(String name) {
		int id;
		try {
			id = Integer.parseInt(name);
		} catch (NumberFormatException ignored) {
			return name;
		}
		return MappingUtil.debobfuscate(((Class) Packet.packetIdToClassMap.lookup(id)).getName()).replace("net.minecraft.network.packet.Packet", "");
	}

	public static void record(final Packet packet) {
		if (!profiling) {
			return;
		}
		String id;
		int size;
		if (packet instanceof Packet250CustomPayload) {
			Packet250CustomPayload packet250CustomPayload = (Packet250CustomPayload) packet;
			id = ((Packet250CustomPayload) packet).channel + (packet250CustomPayload.data.length > 0 ? Byte.toString(packet250CustomPayload.data[0]) : Byte.MIN_VALUE);
			size = packet250CustomPayload.data.length;
		} else {
			id = String.valueOf(packet.getPacketId());
			size = packet.getPacketSize();
		}
		getCount(id).getAndIncrement();
		getSize(id).addAndGet(size);
	}

	private static AtomicInteger getCount(String id) {
		AtomicInteger t = count.get(id);
		if (t == null) {
			synchronized (count) {
				t = count.get(id);
				if (t == null) {
					t = new AtomicInteger();
					count.put(id, t);
				}
			}
		}
		return t;
	}

	private static AtomicInteger getSize(String id) {
		AtomicInteger t = size.get(id);
		if (t == null) {
			synchronized (size) {
				t = size.get(id);
				if (t == null) {
					t = new AtomicInteger();
					size.put(id, t);
				}
			}
		}
		return t;
	}

	// http://stackoverflow.com/a/3758880/250076
	public static String humanReadableByteCount(int bytes) {
		int unit = 1024;
		if (bytes < unit) {
			return bytes + " B";
		}
		int exp = (int) (Math.log(bytes) / Math.log(unit));
		char pre = ("KMGTPE").charAt(exp - 1);
		return String.format("%.1f%cB", bytes / Math.pow(unit, exp), pre);
	}
}

@@ -3799,16 +3799,23 @@
 %0A%09%09%09
-id = ((P
+byte%5B%5D data = p
 acke
@@ -3835,29 +3835,67 @@
 load
-) packet).channel + (
+.data;%0D%0A%09%09%09size = data == null ? 0 : data.length;%0D%0A%09%09%09id = 
 pack
@@ -3913,27 +3913,31 @@
 Payload.
-data.length
+channel + (size
  %3E 0 ? B
@@ -3949,39 +3949,16 @@
 oString(
-packet250CustomPayload.
 data%5B0%5D)
@@ -3982,55 +3982,8 @@
 );%0D%0A
-%09%09%09size = packet250CustomPayload.data.length;%0D%0A
 %09%09%7D 

Fix Packet Profiler NPE