package org.moparscape.msc.gs.core;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import org.apache.mina.common.IoSession;
import org.moparscape.msc.config.Config;
import org.moparscape.msc.config.Constants;
import org.moparscape.msc.config.Formulae;
import org.moparscape.msc.gs.Instance;
import org.moparscape.msc.gs.connection.PacketQueue;
import org.moparscape.msc.gs.connection.RSCPacket;
import org.moparscape.msc.gs.connection.filter.OSLevelBlocking;
import org.moparscape.msc.gs.event.DelayedEvent;
import org.moparscape.msc.gs.event.MiniEvent;
import org.moparscape.msc.gs.model.ActiveTile;
import org.moparscape.msc.gs.model.Item;
import org.moparscape.msc.gs.model.Npc;
import org.moparscape.msc.gs.model.Player;
import org.moparscape.msc.gs.model.Point;
import org.moparscape.msc.gs.model.Shop;
import org.moparscape.msc.gs.model.World;
import org.moparscape.msc.gs.model.snapshot.Snapshot;
import org.moparscape.msc.gs.phandler.PacketHandler;
import org.moparscape.msc.gs.phandler.PacketHandlerDef;
import org.moparscape.msc.gs.plugins.dependencies.NpcAI;
import org.moparscape.msc.gs.tools.Captcha;
import org.moparscape.msc.gs.tools.DataConversions;
import org.moparscape.msc.gs.util.Logger;
import org.moparscape.msc.gs.util.PersistenceManager;

/**
 * The central motor of the game. This class is responsible for the primary
 * operation of the entire game.
 */
public final class GameEngine extends Thread {

	private static Captcha captcha;
	/**
	 * World instance
	 */
	private static final World world = Instance.getWorld();

	public static Captcha getCaptcha() {
		return captcha;
	}

	/**
	 * Responsible for updating all connected clients
	 */
	private ClientUpdater clientUpdater = new ClientUpdater();
	/**
	 * Handles delayed events rather than events to be ran every iteration
	 */
	private DelayedEventHandler eventHandler = new DelayedEventHandler();
	/**
	 * When the update loop was last ran, required for throttle
	 */
	private long lastSentClientUpdate = GameEngine.getTime();
	private long lastSentClientUpdateFast = GameEngine.getTime();
	private long lastCleanedChatlogs = 0;
	private int lastCleanedChatlogsOutput = 0;
	/**
	 * The mapping of packet IDs to their handler
	 */
	private TreeMap<Integer, PacketHandler> packetHandlers = new TreeMap<Integer, PacketHandler>();
	/**
	 * The packet queue to be processed
	 */
	private PacketQueue<RSCPacket> packetQueue;
	/**
	 * Whether the engine's thread is running
	 */
	private boolean running = true;
	private static volatile long time = 0;

	/**
	 * Use this instead of System.currentTimeIllis, as each call does a system
	 * call, and potentially a hardware poll...<br>
	 * Also, you don't generally need the time to be updated more often than
	 * each part in the main loop.
	 * 
	 * @return The current time.
	 */
	public static long getTime() {
		return time;
	}

	/**
	 * Constructs a new game engine with an empty packet queue.
	 */
	public GameEngine() {
		captcha = new Captcha();
		captcha.init();
		packetQueue = new PacketQueue<RSCPacket>();
		loadPacketHandlers();
		for (Shop shop : world.getShops()) {
			shop.initRestock();
		}
		redirectSystemStreams();
	}

	public void emptyWorld() {
		for (Player p : world.getPlayers()) {
			p.save();
			p.getActionSender().sendLogout();
		}
		Instance.getServer().getLoginConnector().getActionSender()
				.saveProfiles();
	}

	/**
	 * Ban dummy packet flood private Map<InetAddress, Long> clients; private
	 * Map<InetAddress, Integer> counts; private Map<InetAddress, Integer>
	 * written; public void flagSession(IoSession session) { InetAddress addr =
	 * getAddress(session); String ip = addr.toString(); ip =
	 * ip.replaceAll("/",""); long now = System.currentTimeMillis(); int c = 0;
	 * if(counts.containsKey(addr) && clients.containsKey(addr)) { try { c =
	 * counts.get(addr); } catch(Exception e) { Logging.debug("Error: " +
	 * e); } if(c >= 10) { if(!written.containsKey(addr)) { try {
	 * Logging.debug("Dummy packet flooder IP: " + ip); BufferedWriter bf2
	 * = new BufferedWriter(new FileWriter("dummy.log", true));
	 * bf2.write("sudo /sbin/route add " + addr.getHostAddress() +
	 * " gw 127.0.0.1"); bf2.newLine(); bf2.close(); written.put(addr, 1); }
	 * catch(Exception e) { System.err.println(e);} } } } if
	 * (clients.containsKey(addr)) { long lastConnTime = clients.get(addr); if
	 * (now - lastConnTime < 2000) { if(!counts.containsKey(addr)) {
	 * counts.put(addr, 0); } else c = counts.get(addr) + 1; counts.put(addr,
	 * c); } else { clients.put(addr, now); } } else { clients.put(addr, now); }
	 * }
	 * 
	 * private InetAddress getAddress(IoSession io) { return
	 * ((InetSocketAddress) io.getRemoteAddress()).getAddress(); }
	 */
	/**
	 * Returns the current packet queue.
	 * 
	 * @return A <code>PacketQueue</code>
	 */
	public PacketQueue<RSCPacket> getPacketQueue() {
		return packetQueue;
	}

	public void kill() {
		Logger.println("Terminating GameEngine");
		running = false;
	}

	/**
	 * Loads the packet handling classes from the persistence manager.
	 */
	protected void loadPacketHandlers() {
		PacketHandlerDef[] handlerDefs = (PacketHandlerDef[]) PersistenceManager
				.load("PacketHandlers.xml");
		for (PacketHandlerDef handlerDef : handlerDefs) {
			try {
				String className = handlerDef.getClassName();
				Class<?> c = Class.forName(className);
				if (c != null) {
					PacketHandler handler = (PacketHandler) c.newInstance();
					for (int packetID : handlerDef.getAssociatedPackets()) {
						packetHandlers.put(packetID, handler);
					}
				}
			} catch (Exception e) {
				Logger.error(e);
			}
		}
	}

	private void processClients() {
		clientUpdater.sendQueuedPackets();
		long now = GameEngine.getTime();
		if (now - lastSentClientUpdate >= 600) {
			if (now - lastSentClientUpdate >= 1000) {
				// Logger.println("MAJOR UPDATE DELAYED: " + (now -
				// lastSentClientUpdate));
			}
			lastSentClientUpdate = now;
			clientUpdater.doMajor();
		}
		if (now - lastSentClientUpdateFast >= 104) {
			if (now - lastSentClientUpdateFast >= 6000) {
				// Logger.println("MINOR UPDATE DELAYED: " + (now -
				// lastSentClientUpdateFast));
			}
			lastSentClientUpdateFast = now;
			clientUpdater.doMinor();
		}
	}

	private void processEvents() {
		eventHandler.doEvents();
	}
	
	public DelayedEventHandler getEventHandler() {
		return eventHandler;
	}

	/**
	 * Redirects system err
	 */
	public static void redirectSystemStreams() {
		OutputStream out = new OutputStream() {
			@Override
			public void write(int b) throws IOException {
				String line = String.valueOf((char) b);
				Logger.systemerr(line);
			}

			@Override
			public void write(byte[] b, int off, int len) throws IOException {
				String line = new String(b, off, len);
				Logger.systemerr(line);
			}

			@Override
			public void write(byte[] b) throws IOException {
				write(b, 0, b.length);
			}
		};
		System.setErr(new PrintStream(out, true));
	}

	private void processIncomingPackets() {
		for (RSCPacket p : packetQueue.getPackets()) {
			IoSession session = p.getSession();
			Player player = (Player) session.getAttachment();
			if (player.getUsername() == null && p.getID() != 32
					&& p.getID() != 77 && p.getID() != 0) {
				final String ip = player.getCurrentIP();
				// flagSession(session);
				OSLevelBlocking.block(ip);
				continue;
			}
			PacketHandler handler = packetHandlers.get(p.getID());
			player.ping();
			if (handler != null) {
				try {
					handler.handlePacket(p, session);
					try {
						if (p.getID() != 5) {
							// String s = "[PACKET] " +
							// session.getRemoteAddress().toString().replace("/",
							// "") + " : " + p.getID()+
							// " ["+handler.getClass().toString()+"]" + " : "+
							// player.getUsername() + " : ";
							// for(Byte b : p.getData())
							// s += b;
							// Logger.println(s);
						}
					} catch (Exception e) {
						e.printStackTrace();
					}

				} catch (Exception e) {
					String s;
					StringWriter sw = new StringWriter();
					PrintWriter pw = new PrintWriter(sw, true);
					e.printStackTrace(pw);
					pw.flush();
					sw.flush();
					s = sw.toString();
					Logger.error("Exception with p[" + p.getID() + "] from "
							+ player.getUsername() + " ["
							+ player.getCurrentIP() + "]: " + s);
					player.getActionSender().sendLogout();
					player.destroy(false);
				}
			} else {
				Logger.error("Unhandled packet from " + player.getCurrentIP()
						+ ": " + p.getID() + "len: " + p.getLength());
			}
		}
	}

	public void processLoginServer() {
		LoginConnector connector = Instance.getServer().getLoginConnector();
		if (connector != null) {
			connector.processIncomingPackets();
			connector.sendQueuedPackets();
		}
	}

	/**
	 * The thread execution process.
	 */
	public void run() {
		Logger.println("GameEngine now running");
		// Captcha.loadCharacters();
		for (Npc n : Instance.getWorld().getNpcs()) {
			for (NpcAI ai : Instance.getPluginHandler().getNpcAI()) {
				if (n.getID() == ai.getID()) {
					n.setScripted(true);
				}
			}
		}
		time = System.currentTimeMillis();

		eventHandler.add(new DelayedEvent(null, Config.GARBAGE_COLLECT_INTERVAL) { // Ran every
																	// 50*2
																	// minutes
					@Override
					public void run() {
						new Thread(new Runnable() {
							public void run() {
								garbageCollect();
							}
						}).start();
					}
				});
		eventHandler.add(new DelayedEvent(null, Config.SAVE_INTERVAL) { // 5 min
					public void run() {
						world.dbKeepAlive();
						long now = GameEngine.getTime();
						for (Player p : world.getPlayers()) {
							if (now - p.getLastSaveTime() >= Config.SAVE_INTERVAL) {
								p.save();
								p.setLastSaveTime(now);
							}
						}
						Instance.getServer().getLoginConnector()
								.getActionSender().saveProfiles();
					}
				});
		while (running) {
			try {
				Thread.sleep(50);
			} catch (InterruptedException ie) {
			}
			time = System.currentTimeMillis();
			long Delay;
			processLoginServer();
			Delay = System.currentTimeMillis() - time;
			if (Delay >= 1000)
				Logger.println("processLoginServer is taking longer than it should, exactly "
						+ Delay + "ms");
			time = System.currentTimeMillis();
			processIncomingPackets();
			Delay = System.currentTimeMillis() - time;
			if (Delay >= 1000)
				Logger.println("processIncomingPackets is taking longer than it should, exactly "
						+ Delay + "ms");
			time = System.currentTimeMillis();
			processEvents();
			Delay = System.currentTimeMillis() - time;
			if (Delay >= 1000)
				Logger.println("processEvents is taking longer than it should, exactly "
						+ Delay + "ms");
			time = System.currentTimeMillis();
			processClients();
			Delay = System.currentTimeMillis() - time;
			if (Delay >= 1000)
				Logger.println("processClients is taking longer than it should, exactly "
						+ Delay + "ms");
			time = System.currentTimeMillis();
			cleanSnapshotDeque();
			Delay = System.currentTimeMillis() - time;
			if (Delay >= 1000)
				Logger.println("processSnapshotDeque is taking longer than it should, exactly "
						+ Delay + "ms");
		}
	}

	/**
	 * Cleans snapshots of entries over 60 seconds old (executed every second)
	 */
	public void cleanSnapshotDeque() {
		long curTime = GameEngine.getTime();
		if (curTime - lastCleanedChatlogs > 1000) {
			lastCleanedChatlogs = curTime;
			lastCleanedChatlogsOutput++;
			if (lastCleanedChatlogsOutput > 60 * 5) {
				Logger.println("----------------------------------------------");
				Logger.println(world.getSnapshots().size() + " items on deque");
			}
			Iterator<Snapshot> i = world.getSnapshots().descendingIterator();
			Snapshot s = null;
			while (i.hasNext()) {
				s = i.next();
				if (curTime - s.getTimestamp() > 60000) {
					i.remove();
					s = null;
				} else {
					s = null;
				}
			}
			i = null;
			if (lastCleanedChatlogsOutput > 60 * 5) {
				Logger.println(world.getSnapshots().size()
						+ " items on deque AFTER CLEANUP");
				Logger.println("----------------------------------------------");
				lastCleanedChatlogsOutput = 0;
			}
		}
	}

	/**
	 * Cleans garbage (Tilecleanup)
	 */
	public synchronized void garbageCollect() {
		long startTime = System.currentTimeMillis();
		int curMemory = (int) (Runtime.getRuntime().totalMemory() - Runtime
				.getRuntime().freeMemory()) / 1000;
		int tileObjs = 0;
		int cleaned = 0;
		for (int i = 0; i < Instance.getWorld().tiles.length; i++) {
			for (int in = 0; in < Instance.getWorld().tiles[i].length; in++) {
				ActiveTile tile = Instance.getWorld().tiles[i][in];
				if (tile != null) {
					tileObjs++;
					if (!tile.hasGameObject() && !tile.hasItems()
							&& !tile.hasNpcs() && !tile.hasPlayers()) {
						Instance.getWorld().tiles[i][in] = null;
						cleaned++;
					}
				}
			}
		}
		Runtime.getRuntime().gc();
		int newMemory = (int) (Runtime.getRuntime().totalMemory() - Runtime
				.getRuntime().freeMemory()) / 1000;
		Logger.println("GARBAGE COLLECT | Executing Memory Cleanup");
		Logger.println("GARBAGE COLLECT | Memory before: " + curMemory + "kb"
				+ " Memory after: " + newMemory + " (Freed: "
				+ (curMemory - newMemory) + "kb)");
		Logger.println("GARBAGE COLLECT | Cleanup took "
				+ (System.currentTimeMillis() - startTime) + "ms");
	}
}
