package net.woodyfolsom.msproj;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.Properties;

import net.woodyfolsom.msproj.Command.TYPE;
import net.woodyfolsom.msproj.policy.AlphaBeta;
import net.woodyfolsom.msproj.policy.Minimax;
import net.woodyfolsom.msproj.policy.MonteCarloUCT;
import net.woodyfolsom.msproj.policy.Policy;
import net.woodyfolsom.msproj.policy.RandomMovePolicy;

import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;

import com.gokgs.client.gtp.GtpClient;
import com.gokgs.client.gtp.Options;

public class GoGame implements Runnable {
	private static final String PROPS_FILE = "kgsGtp.ini";
	private static final int INVALID_MOVE_GENERATOR = 1;
	private static final Logger LOGGER = Logger.getLogger(GoGame.class
			.getName());
	
	private boolean shutDown = false;
	private GameConfig gameConfig = new GameConfig(9,7.5);
	private GameState gameState = new GameState(gameConfig);
	private GtpClient client = null;
	private Policy moveGenerator;
	private Properties properties;
	
	private PipedOutputStream remoteOutput = new PipedOutputStream();
	private PipedInputStream localInput = new PipedInputStream(remoteOutput);
	
	private PipedOutputStream localOutputInner = new PipedOutputStream();
	private PipedInputStream remoteInput = new PipedInputStream(localOutputInner);
	private PrintWriter localOutput = new PrintWriter(localOutputInner);
	
	public OutputStream getOutputStream() {
		return remoteOutput;
	}
	
	public InputStream getInputStream() {
		return remoteInput;
	}
	
	public Properties getProperties() {
		return properties;
	}
	
	public GoGame(Policy moveGenerator, String propFileName) throws IOException {
		this.moveGenerator = moveGenerator;
		this.properties = new Properties();
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(propFileName);
			properties.load(fis);
		} finally {
			if (fis != null) {
				try {
					fis.close();
				} catch (IOException ioe) {
					LOGGER.info(ioe.getMessage());
				}
			}
		}
	}
	
	public void run() {
		play();
	}
	
	public static void main(String[] args) throws IOException {
		configureLogging();
		if (args.length == 0) {
			Policy defaultMoveGenerator = new MonteCarloUCT(new RandomMovePolicy(), 5000L);
			LOGGER.info("No MoveGenerator specified.  Using default: " + defaultMoveGenerator.toString());
			
			GoGame goGame = new GoGame(defaultMoveGenerator, PROPS_FILE);
			new Thread(goGame).start();
			
			System.out.println("Creating GtpClient");
			goGame.client = new GtpClient(goGame.getInputStream(), goGame.getOutputStream(), new Options(goGame.getProperties(), LOGGER.getName()));
			
			System.out.println("GtpClient created");
			
			System.out.println("Invoking GtpClient.go()...");
			
			if (goGame.client.go()) {
				System.out.println("GtpClient.go() succeeded.");
			} else {
				System.out.println("GtpClient.go() failed.");
			}
		} else {
			new GoGame(createPolicy(args[0]), PROPS_FILE).play();
		}
	}

	private static Policy createPolicy(String policyName) {
		if ("random".equals(policyName)) {
			return new RandomMovePolicy();
		} else if ("minimax".equals(policyName)) {
			return new Minimax();
		} else if ("alphabeta".equals(policyName)) {
			return new AlphaBeta();
		} else if ("montecarlo".equals(policyName)) {
			return new MonteCarloUCT(new RandomMovePolicy(), 5000L);
		} else {
			LOGGER.info("Unable to create Policy for unsupported name: " + policyName);
			System.exit(INVALID_MOVE_GENERATOR);
			//This line will never be executed but prevents the compiler from complaining.
			return null;
		}
	}
	
	private void executeCommand(Command cmd) {
		switch (cmd.getType()) {
		case INVALID:
			LOGGER.info("Invalid command ignored: " + cmd.getText());
			localOutput.println("? Invalid command: " + cmd.getText() + "\n");
			break;
		case boardsize:
			gameConfig = new GameConfig(cmd.getIntField(1));
			gameState = new GameState(gameConfig);
			localOutput.println("=\n");
			break;
		case clear_board:
			gameState.clearBoard();
			localOutput.println("=\n");
			break;
		case final_status_list:
			System.out.println("Final game score: ");
			System.out.println(gameState.getResult());
			System.out.println();
			localOutput.println("=\n");
			break;
		case genmove:
			System.out.println("Generating move for:\n" + gameState);
			System.out.println("It is currently " + gameState.getPlayerToMove() + "'s turn.");
			String playerName = cmd.getStringField(1);
			
			Player player;
			if ("b".equals(playerName)) {
				player = Player.BLACK;
			} else if ("w".equals(playerName)) {
				player = Player.WHITE;
			} else {
				throw new RuntimeException("Invalid player name - cannot generator move for:" + cmd);
			}
			
			Action nextMove = moveGenerator.getAction(gameConfig, gameState,
					player);
			
			gameState.playStone(player, nextMove);
			LOGGER.info(gameState.getResult());
			localOutput.println("="
					+ nextMove.toString() + "\n");
			
			break;
		case komi:
			gameConfig.setKomi(cmd.getDoubleField(1));
			localOutput.println("=\n");
			break;
		case list_commands:
			StringBuilder sb = new StringBuilder("= ");
			for (TYPE type : Command.TYPE.values()) {
				if (type != TYPE.INVALID) {
					sb.append(type.toString());
					sb.append("\n");
				}
			}
			System.out.println("Sending response: " + sb.toString());
			localOutput.println(sb.toString());
			System.out.println("Response sent.");
			break;
		case name:
			localOutput.println("= CS6601P1\n");
			break;
		case quit:
			localOutput.println("=\n");
			shutDown = true;
			break;
		case play:
			if (gameState.playStone(Player.getInstance(cmd.getStringField(1)), cmd
					.getStringField(2).toUpperCase())) {
				localOutput.println("=\n");
			} else {
				String errMsg = "Unable to make requested play: "
						+ cmd.getText();
				LOGGER.info(errMsg);
				LOGGER.info("GameState: " + gameState);
				localOutput.println("?" + errMsg + "\n");
			}
			LOGGER.info(gameState.getResult());
			break;
		case version:
			localOutput.println("= 0.1\n");
			break;
		default:
			LOGGER.warn("An error occured.  Unhandled command: " + cmd);
		}
		localOutput.flush();
	}

	public void play() {
		byte[] buf = new byte[128];
		int inputLength;
		try {
			while (!shutDown && (inputLength = localInput.read(buf)) > 0) {
				System.out.println("Waiting for input");
				
				String commandText = new String(buf, 0, inputLength,
						Charset.forName("UTF-8"));
				
				System.out.println("Input received: " + commandText);
				
				LOGGER.info("Command received: " + commandText);
				
				Command cmd = CommandParser.parse(commandText);
				
				executeCommand(cmd);
				
				System.out.println("It is now " + gameState.getPlayerToMove() + "'s turn.\n");
				
				if (cmd.getType() == Command.TYPE.genmove) {
					System.out.println("New game state:\n" + gameState);
				}
			}
		} catch (IOException ioe) {
			LOGGER.warn(ioe.getMessage());
		}
	}

	private static void configureLogging() {
		DOMConfigurator.configure("log4j.xml");
	}
	
	public static Player getNextPlayer(Player player) {
		if (player == Player.WHITE) {
			return Player.BLACK;
		} else if (player == Player.BLACK) {
			return Player.WHITE;
		} else {
			return Player.NONE;
		}
	}
}