import java.net.*;
import java.io.*;
import java.util.Random;
import java.util.Date;

class BattleshipServer {
  Players players = null;
  PrintStream log;

  BattleshipServer() throws IOException {
    log = new PrintStream(
	     new BufferedOutputStream(
                new FileOutputStream("battleship.log")));
						      
    players = new Players();
  }

  public static void main(String[] args) {
    BattleshipServer s = null;
    try {
      s = new BattleshipServer();
      s.runServer();
    }
    catch (IOException e) {
      s.log.println("IO Exeption " + e.getMessage());
    }
  }

  void log(String s) {
    log.println(s);
    log.flush();
  }

  public void runServer() {
    ServerSocket serverSocket = null;
    int port = 4444;
    try {
      serverSocket = new ServerSocket(port);
    }
    catch (IOException e) {
      System.err.println("Listen error on port " + port);
      System.exit(1);
    }
    // start the server thread
    ServerThread st = new ServerThread(this);
    st.start();
    // now th listening loop
    boolean listening = true;
    while (listening) {
      Socket clientSocket = null;
      System.out.println("Listening...");
      try {
	clientSocket = serverSocket.accept();
	//Trace.p("Got client");
      }
      catch (IOException e) {
	System.err.println("Accept failed on " + port +
			   ", " + e.getMessage());
	continue;
      }
      ClientThread thread = new ClientThread(clientSocket, this);
      thread.start();
    }
    try {
      serverSocket.close();
    }
    catch (IOException e) {
      e.printStackTrace();
    }
  }
}

class Message {
  String message;
  int id;

  Message(String m, int id) {
    this.message = m;
    this.id = id;
  }
}

class ServerThread extends Thread {
  BattleshipServer server;

  ServerThread(BattleshipServer b) {
    server = b;
  }

  public void run() {
    server.log("ServerThread running");
    boolean forever = true;
    Message output;
    while (forever) {
      Player p = server.players.getPlayerWithMessage(server.log);
      if (p != null) {
	output = p.thread.processInput(p.inBox, p.id);
	if (output != null) {
	  if (output.message.equals("quit")) {
	    server.log("Quit from " + p.id + " to " + output.id);
	    //server.players.putOutbox("", output.id); // ?????
	    p.thread.stop();
	  }
	  else if (output.message.equals("quit?")) {
	    // quit? should be sent to both players
	    server.log("Acknowledging quit? from " + p.id);
	    server.players.putOutbox("quit?", p.id);
	    server.log("Sending quit request to " + output.id);
	    server.players.putOutbox(output.message, output.id);
	  }
	  else {
	    server.players.putOutbox(output.message, output.id);
	  }
	}
      }
    }
  }
}


class ClientThread extends Thread {
  private Socket socket = null;
  private BattleshipServer server;
  private int id;
  ServerConnectState cs;
  Date date;  // last message processed
  String time = new Date().toString();
  
  ClientThread(Socket s, BattleshipServer b) {
    super("ClientThread");
    id = b.players.getID();
    socket = s;
    server = b;
  }

  int getID() {
    return id;
  }

  public void run() {
    // make a dummy player
    Player p = new Player(null, id, this);
    server.players.addPlayer(p, server.log);
    // make a protocol handler
    cs = new ServerConnectState(server);
    // initialize the time
    date = new Date();

    System.out.println("Client " + id + " thread running");
    try {
      DataInputStream is = new DataInputStream(
			       new BufferedInputStream(
				  socket.getInputStream()));
      PrintStream os = new PrintStream(
		          new BufferedOutputStream(
			     socket.getOutputStream(),
			     1024), false);
      String input, output;
      Message start = processInput(null, id);
      os.println(start.message);
      os.flush();
      boolean stillConnected = true;
      while (stillConnected) {
	input = is.readLine();
	//Trace.p("Putting " + input + " for " + id);
	server.players.putInbox(id, input);
	date = new Date();
	output = server.players.getOutbox(id);
	//	Trace.p("Getting " + output + " for " + id);
	os.println(output);
	os.flush();
      }
      os.close();
      is.close();
      socket.close();
    }
    catch (IOException e) {
      server.log("IO Exception " + e.getMessage() + " for " + id);
    }
    server.log("Exiting connect thread for " + id);
  }

  Message processInput(String m, int id) {
    return cs.processInput(m, id);
  }
}

class ServerConnectState {
  BattleshipServer server;
  private static final int STARTUP = 0;
  private static final int GOTNAME = 1;
  private static final int GOTLIST = 2;
  private static final int PLAY = 3;
  private static final int ADMIN = 4;

  private int state = STARTUP;

  ServerConnectState(BattleshipServer b) {
    server = b;
  }

  Message processInput(String input, int id) {
    String output = null;
    Player proposer, partner;
    String command = null;
    String rest = null;
    if (input != null) {
      int ind = input.indexOf(" ");
      if (ind != -1) {
	command = input.substring(0, ind);
	rest = input.substring(ind + 1);
      }
      else
	command = input;
    }

  switch (state) {
    case STARTUP:
      if (command == null) {
	output = "name?";
	state = GOTNAME;
	server.log("Processing " + input + " for " + id + " in state " + state);
	server.log("Processed to: " + output + " for " + id);
	return new Message(output, id);
      }
    case GOTNAME:
      if (command.equals("nameis")) {
	// set the player's name
	if (rest == null)
	  rest = "WHO" + id;
	else if (rest.equals("fred01")) {
	  state = ADMIN;
	  server.players.setName(id, "Admin");
	  output = "list " + server.players.getAllNames();
	  server.log("Processing " + input + " for " + id + " in state " + state);
	  server.log("Processed to: " + output + " for " + id);
	  return new Message(output, id);
	}
	server.players.setName(id, rest);
	state = GOTLIST;
	// send list of unconnected names
	output = "list " + server.players.getUnconnectedNames(rest);
	server.log("Processing " + input + " for " + id + " in state " + state);
	server.log("Processed to: " + output + " for " + id);
	return new Message(output, id);
      }
      else {
	return new Message("name?", id);
      }
  case GOTLIST:
    proposer = server.players.getPlayer(id);
    if (command.equals("choose")) {
      if (rest != null && !rest.equals("") && !rest.equals(" ") &&
	  !rest.equalsIgnoreCase("none")) {
	Player acceptor = server.players.getPlayer(rest);
	if (acceptor != null) {
	  server.players.connect(proposer, acceptor, server.log);
	  output = "connect? " + proposer.name;
	  //Trace.p("Acceptor is " + acceptor.name + "(" + acceptor.id + ")");
	  state = PLAY;
	  server.log("Processing " + input + " for " + id + " in state " + state);
	  server.log("Processed to: " + output + " for " + acceptor.id);
	  return new Message(output, acceptor.id);
	}
	else {
	  server.log("Couldn't find acceptor " + rest);
	}
      }
    }
    else {
      partner = server.players.getConnection(id);
      if (command.equals("yes")) {
	state = PLAY;
	// send first guess to random player
	Random gen = new Random();
	int who = (int)(gen.nextDouble() + 0.5);
	int nid;
	if (who == 0)
	  nid = id;
	else
	  nid = partner.id;
	output = "guess?";
	server.log("Processing " + input + " for " + id + " in state " + state);
	server.log("Processed to: " + output + " for " + nid);
	return new Message(output, nid);
      }
      else if (command.equals("no")) {
	Player quitter = server.players.getPlayer(id);
	server.players.disconnect(quitter, partner, server.log);
	output = "quit?";
	server.log("Processing " + input + " for " + id + " in state " + state);
	server.log("Processed to: " + output + " for " + partner.id);
	return new Message(output, partner.id);
      }
      else if (command.equals("quit")) {
	Player quitter = server.players.getPlayer(id);
	if (partner != null)
	  server.players.disconnect(quitter, partner, server.log);
	server.players.removePlayer(quitter, server.log);
	// so that the server can stop the thtread
	if (partner != null)
	  return new Message(command, partner.id); 
	else
	  return new Message(command, id);
      }
    }
    // send the list again for any other repsonse or no acceptor
    // Trace.p("Sending list again");
    output = "list " +
      server.players.getUnconnectedNames(proposer.name);
    if (!output.equals("list ")) {
      server.log("Processing " + input + " for " + id + " in state " + state);
      server.log("Processed to: " + output + " for " + id);
    }
    return new Message(output, id);
  case PLAY:
    partner = server.players.getConnection(id);
    if (command.equals("quit")) {
      Player quitter = server.players.getPlayer(id);
      if (partner != null) {
	server.players.disconnect(quitter, partner, server.log);
	server.players.removePlayer(quitter, server.log);
	server.log("Processing " + input + " for " + id + " in state " + state);
	return new Message("quit?", partner.id);
      }
      else {
	// just remove the player
	server.players.removePlayer(quitter, server.log);
	server.log("Processing " + input + " for " + id + " in state " + state);
	return null; // player has quit anyway
      }
    }
    else if (command.equals("new")) {
      // Trace.p("Start over: sending list again");
      proposer = server.players.getPlayer(id);
      state = GOTLIST;
      if (partner != null) {
	server.players.disconnect(proposer, partner, server.log);
	server.log("Processing " + input + " for " + id + " in state " + state);
	return new Message("quit?", partner.id);
      }
      else { // we sent ourselves new, so send list again
	output = "list " +
	  server.players.getUnconnectedNames(proposer.name);
	server.log("Processing " + input + " for " + id + " in state " + state);
	server.log("Processed to: " + output + " for " + id);
	return new Message(output, id);
      }
    }
    else if (partner != null) { 
      // just send the message to the other player
      server.log("Processing " + input + " for " + id + " in state " + state);
      return new Message(input, partner.id);
    }
    else {
      server.log("Partner disconnected early?");
      return null; // should never happen
    }
  case ADMIN:
    if (command.equals("delete")) {
      int index = rest.indexOf(" ");
      int last = 0;
      String delName;
      while (index != -1) {
	delName = rest.substring(last, index);
	Player old = server.players.getPlayer(delName);
	if (old != null) {
	  server.players.removePlayer(old, server.log);
	  server.log("Deleting " + delName);
	}
	last = index + 1;
	index = rest.indexOf(" ", last);
      }

    }
    else if (command.equals("quit")) {
      Player quitter = server.players.getPlayer(id);
      server.players.removePlayer(quitter, server.log);
      return new Message(input, id);
    }
    output = "list " + server.players.getAllNames();
    return new Message(output, id);
    
  }
  // if no message can be found, ignore it
  return null;
  }
}

class Player {
  String name;
  ClientThread thread;
  String inBox;
  String outBox;
  int id;
    
  Player(String name, int id, ClientThread t) {
    this.name = name;
    this.id = id;
    this.thread = t;
    this.inBox = null;
    this.outBox = null;
  }

  Player(String name, int id, String in, String out, ClientThread t) {
    this.name = name;
    this.id = id;
    this.inBox = in;
    this.outBox = out;
    this.thread = t;
  }
}

class Players {
  private Player[][] players = new Player[2][100];
  private int nPlayers = 0;
  private int currentPlayer = 0; // used by getMessage


  // the id service
  private static int id = 0;

  public static int getID() {
    return id++;
  }

  public synchronized Player getPlayerWithMessage(PrintStream log) {
    // return the first player who has a non-empty inBox
    boolean notFound = true;
    Player p = null;
    int n = currentPlayer;
    while (notFound) {
      if (nPlayers > 0)
	while (notFound) {
	  p = players[0][n];
	  if (p != null && p.inBox != null)
	    notFound = false;
	  else  {
	    ++n;
	    if (n == nPlayers)
	      n = 0;
	    if (n == currentPlayer)
	      break;
	  }
	}
      if (notFound) {
	try {
	  wait();
	}
	catch (InterruptedException e) {}
      }
    }
    currentPlayer = n;
    Player np = new Player(p.name, p.id, p.inBox, p.outBox, p.thread);
    p.inBox = null;
    notifyAll();
    //Trace.p("Got message for " + p.id + "(" + p.inBox + ")");
    //Trace.p("current is now " + currentPlayer);
    return np;
  }

  public synchronized void putOutbox(String m, int id) {
    for (int n = 0; n < nPlayers; n++) {
      Player p = players[0][n];
      if (p.id == id) {
	p.outBox = m;
	//Trace.p("Put outBox for " + id + "(" + m + ")");
	notifyAll();
      }
    }
  }

  public synchronized void putInbox(int id, String m) {
    for (int n = 0; n < nPlayers; n++) {
      Player p = players[0][n];
      //Trace.p("putInbox " + p.name + "," + p.id + "," + p.inBox +"," +  p.outBox);
      if (p.outBox != null) {
	//Trace.p("Ignoring " + p.inBox + " from " + p.name);
      }
      else if (p.id == id) {
	p.inBox = m;
	//Trace.p("Put inBox for " + id + "(" + m + ")");
	notifyAll();
      }
    }
  }

  public synchronized String getOutbox(int id) {
    int n;
    Player p = null;
    for (n = 0; n < nPlayers; n++) {
      p = players[0][n];
      //Trace.p("getOutbox " + p.name + "," + p.id + "," + p.inBox +"," +  p.outBox);
      if (p.id == id)
	break;
    }
    while (p.outBox == null) {
      try {
	//Trace.p("Waiting for outBox for " + id);
	wait();
      }
      catch (InterruptedException e) {}
    }
    String m = p.outBox;
    p.outBox = null;
    notifyAll();
    //Trace.p("Got outBox for " + id + "(" + m + ")");
    return m;
  }

  public synchronized Player getPlayer(String name) {
    for (int n = 0; n < nPlayers; n++)
      if (players[0][n].name.equals(name))
	return players[0][n];
	return null;
  }
  
  public synchronized Player getPlayer(int id) {
    for (int n = 0; n < nPlayers; n++) {
      Player p = players[0][n];
      if (p.id == id)
	return p;
    }
    return null;
  }

  public synchronized Player getConnection(int id) {
  for (int n = 0; n < nPlayers; n++) {
      Player p1 = players[0][n];
      if (p1.id == id) {
	return players[1][n];
      }
    }
    return null;
  }
    
  
  public synchronized void addPlayer(Player p, PrintStream log) {
    log.println("Adding " + p.name);
    log.flush();
    players[0][nPlayers] = p;
    players[1][nPlayers++] = null;
  }

  public synchronized void removePlayer(Player p, PrintStream log) {
    log.println("Removing " + p.name + nPlayers + currentPlayer);
    log.flush();
    for (int n = 0; n < nPlayers; n++)
      if (players[0][n].id == p.id) {
	for (int r = nPlayers - 1; r > n; r--) {
	  players[0][r - 1] = players[0][r];
	  players[1][r - 1] = players[1][r];
	}
	--nPlayers;
	if (currentPlayer >= n && currentPlayer > 0)
	  --currentPlayer;
      }
  }

  public synchronized void setName(int id, String name) {
    for (int n = 0; n < nPlayers; n++) {
      Player p = players[0][n];
      if (p.id == id) {
	//Trace.p ("Setting name for " + id + "(" + name + ")");
	p.name = name;
      }
    }
  }
  
  public synchronized void connect(Player p1, Player p2, PrintStream log) {
    log.println("Connecting " + p2.name + " to " + p1.name);
    log.flush();
    for (int n = 0; n < nPlayers; n++)
      if (players[0][n].id == p1.id)
	players[1][n] = p2;
      else if (players[0][n].id == p2.id)
	players[1][n] = p1;
  }

  public synchronized void disconnect(Player p1, Player p2, PrintStream log) {
   log.println("Disconnecting " + p1.name + " and " + p2.name);
   log.flush();
    for (int n = 0; n < nPlayers; n++) {
      if (players[0][n].id == p1.id)
	players[1][n] = null;
      else if (players[0][n].id == p2.id)
	players[1][n] = null;
    }
  }
  
  public synchronized String getUnconnectedNames(String notThis) {
    //Trace.p("Getting unconnected names for " + notThis);
    String names = "";
    for (int n = 0; n < nPlayers; n++) {
      Player p = players[0][n];
      if (players[1][n] != null)
	continue;
      if (p.name != null) {
	//Trace.p("<" + p.name + "><" + notThis + ">");
	if (!p.name.equals(notThis) && !p.name.equals("Admin"))
	  names = names + p.name + " ";
      }
    }
    return names;
  }

  public synchronized String getAllNames() {
    String names = "";
    for (int n = 0; n < nPlayers; n++) {
      Player p = players[0][n];
      if (p.name != null && !p.name.equals("Admin")) {
	  names = names + p.name + " " + p.thread.date.toString() + "@";

      }
    }
    return names;
    
  }
}


        

