package org.bwapi.proxy;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;

import org.bwapi.proxy.messages.BasicTypes.BoxedBoolean;
import org.bwapi.proxy.messages.BasicTypes.VoidT;
import org.bwapi.proxy.messages.GameMessages.StaticGameData;
import org.bwapi.proxy.messages.GameMessages.UnitId;
import org.bwapi.proxy.messages.Messages.FrameCommands;
import org.bwapi.proxy.messages.Messages.FrameMessage;
import org.bwapi.proxy.messages.Messages.PlayerText;
import org.bwapi.proxy.model.Game;
import org.bwapi.proxy.model.Position;
import org.bwapi.proxy.model.Bwta;

import com.google.protobuf.AbstractMessage;
import com.google.protobuf.AbstractMessageLite;
import com.google.protobuf.ByteString;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.Message;
import com.google.protobuf.AbstractMessage.Builder;

import edu.berkeley.nlp.starcraft.overmind.Overmind;

public class ProxyServer implements Runnable {
	/** port to start the server socket on */
	public static final int port = 12345;
	private ProxyBotFactory factory;

	public ProxyServer(ProxyBotFactory factory) {
	  this.factory = factory;
  }

	public static void main(String[] args) {
		ProxyBotFactory factory = getFactory(args);
		new ProxyServer(factory).run();

	}

	private static ProxyBotFactory getFactory(String[] args) {
		return new ProxyBotFactory() {

			@Override
			public ProxyBot getBot(Game g) {
				return new Overmind();
			}
			
		};
	}

	/**
	 * Starts the ProxyBot.
	 * 
	 * A server socket is opened and waits for client connections.
	 */
	public void run() {
		ServerSocket serverSocket = null;
		try {
			serverSocket = new ServerSocket(port);
			serverSocket.setSoTimeout(2000);
			while (!Thread.interrupted()) {
				System.out.println("Waiting for client connection");
				while(!Thread.interrupted()) {
					try {
						Socket clientSocket = serverSocket.accept();
						System.out.println(clientSocket.isConnected());

						System.out.println("Client connected");
						runGame(clientSocket);
						break;
					} catch(SocketTimeoutException ste) {
						continue;
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if(serverSocket != null)
	      try {
	        serverSocket.close();
        } catch (IOException e) {
	        e.printStackTrace();
        }
		}
	}

	private <T> T readMessage(InputStream input, Builder<?> b) throws IOException {
		b.mergeDelimitedFrom(input);
		return (T) b.build();
	}

	byte[] intToBytes(int i) {
		ByteBuffer bb = ByteBuffer.allocate(4);
		bb.putInt(i);
		return bb.array();
	}

	private void writeMessage(OutputStream output, Message m) throws IOException {
		output.write(intToBytes(m.getSerializedSize()));
		m.writeTo(output);
		output.flush();
	}

	/**
	 * Manages communication with StarCraft.
	 */
	private void runGame(Socket socket) {
		ProxyBot bot = null;
		try {
			// 1. get the initial game information
			InputStream input = new BufferedInputStream(socket.getInputStream());
			OutputStream output = new BufferedOutputStream(socket.getOutputStream());
			StaticGameData data = readMessage(input, StaticGameData.newBuilder());
			Game g = Game.getInstance();
			g.init();
			g.readStaticGameData(data);

//			System.out.println(data.toString());

			// send an ACK.
			writeMessage(output, BoxedBoolean.newBuilder().setIsWinner(false).build());
			
			
			// 4. game updates
			boolean won = false;
			while (!Thread.interrupted()) {
				FrameMessage frame = readMessage(input, FrameMessage.newBuilder());
			//	System.out.println(frame.toString());

				if(frame.hasTerrainInfo()) {
					Bwta.getInstance().setTerrainData(frame.getTerrainInfo());
				}
				g.readFrameMessage(frame);

				if (bot == null) {
					bot = factory.getBot(g);
					bot.onStart();
				}
				processCallbacks(frame, bot, g);

				if (frame.getGameover()) {
					 won = frame.hasIsWinner() && frame.getIsWinner();
					break;
				}

				bot.onFrame();
				
				FrameCommands commands = g.flushCommands();
				writeMessage(output, commands);
				output.flush();

			}
			
			if(bot != null)
				bot.onEnd(won);
			
			if(Thread.interrupted()) {
				System.out.println("Thread Interrupted.");
				return;
			}

			// wait for game to terminate


			System.out.println("Game Ended");
		} catch (SocketException e) {
			System.out.println("StarCraft has disconnected");
			if(bot != null) bot.onDroppedConnection();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
		}
	}

	private void processCallbacks(FrameMessage frame, ProxyBot bot, Game g) {
		for (String sentText : frame.getSentTextList()) {
			bot.onSendText(sentText);
		}
		for (PlayerText pt: frame.getReceivedTextsList()) {
			bot.onReceiveText(g.getPlayer(pt.getPlayer()), pt.getText());
		}
		for (int id : frame.getLeftplayersList()) {
			bot.onPlayerLeft(g.getPlayer(id));
		}
		for (UnitId u : frame.getCreatedUnitsList()) {
			bot.onUnitCreate(g.getUnit(u.getId()));
		}
		for (UnitId u : frame.getDestroyedUnitsList()) {
			bot.onUnitDestroy(g.getUnit(u.getId()));
		}
		for (UnitId u : frame.getHiddenUnitsList()) {
			bot.onUnitHide(g.getUnit(u.getId()));
		}
		for (UnitId u : frame.getShownUnitsList()) {
			bot.onUnitShow(g.getUnit(u.getId()));
		}
		for (UnitId u : frame.getMorphedUnitsList()) {
			bot.onUnitMorph(g.getUnit(u.getId()));
		}
		for (UnitId u : frame.getRenegadedUnitsList()) {
			bot.onUnitRenegade(g.getUnit(u.getId()));
		}
		if (frame.hasNukeDetect()) {
			bot.onNukeDetect(new Position(frame.getNukeDetect().getX(), frame.getNukeDetect().getY()));
		}
	}
}
