
import java.io.*;
import java.util.*;
import java.net.*;

public class BlobWarsServer
{
    public static final int PORT = 38491;

    private ServerSocket m_socket;
    private BlobWarsWorld m_world;
    private Vector<ClientUpdate> m_clientUpdates;
    private HashMap<Socket, String> m_socketToPlayerName;
    private HashMap<String, Player> m_lastPlayers;
    private HashMap<Integer, Shot> m_lastShots;
    private int m_lastNumPlayersAlive;

    public static void main(String[] args)
    {
        BlobWarsServer bws = new BlobWarsServer();
        bws.mainloop();
    }

    public BlobWarsServer()
    {
        m_lastNumPlayersAlive = 0;
        m_world = new BlobWarsWorld();
        m_clientUpdates = new Vector<ClientUpdate>();
        m_socketToPlayerName = new HashMap<Socket, String>();
        try {
            m_socket = new ServerSocket(PORT);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
    }

    private class ClientUpdate
    {
        public Socket socket;
        public String message;
    }

    public void mainloop()
    {
        if (m_socket == null)
            return;

        new Thread(new ServerGameLoop()).start();

        /* listen loop to accept new connections */
        for (;;)
        {
            Socket client;
            try {
                client = m_socket.accept();
            } catch (Exception e) {
                System.out.println("Socket error!");
                e.printStackTrace();
                break;
            }

            /* handle the client in a separate thread */
            Thread t = new Thread(new ClientHandler(client));
            t.start();
        }
    }

    private void processClientUpdate(ClientUpdate cu)
    {
        StringTokenizer st = new StringTokenizer(cu.message, ":");
        Vector<String> tokens = new Vector<String>();
        while (st.hasMoreTokens())
        {
            tokens.add(st.nextToken());
        }
        String[] arr = tokens.toArray(new String[1]);
        if (arr.length < 1)
            return;
        if (arr[0].equals("NAME"))
        {
            if (arr.length < 2)
                return;
            if (m_world.getPlayers().containsKey(arr[1]))
            {
                /* this player name is already registered */
                try {
                    BufferedWriter bw = new BufferedWriter(
                            new OutputStreamWriter(
                                cu.socket.getOutputStream()));
                    String sendLine = "NAMEEXISTS\n";
                    bw.write(sendLine, 0, sendLine.length());
                    bw.flush();
                    cu.socket.close();
                } catch (Exception e) {}
                return;
            }
            System.out.println("Player '" + arr[1] + "' signed on.");
            m_world.addPlayer(arr[1]);
            m_socketToPlayerName.put(cu.socket, arr[1]);
            m_lastPlayers.clear();  // clearing the last information will
            m_lastShots.clear();    // push everything to the new player
            return;
        }
        /* the rest of the commands all require a player to be registered */
        if (!m_socketToPlayerName.containsKey(cu.socket))
            return;
        String playerName = m_socketToPlayerName.get(cu.socket);
        if (arr[0].equals("SHOOT")) {
            m_world.shoot(playerName);
        } else if (arr[0].equals("UP")) {
            m_world.moveUp(playerName);
        } else if (arr[0].equals("DOWN")) {
            m_world.moveDown(playerName);
        } else if (arr[0].equals("LEFT")) {
            m_world.moveLeft(playerName);
        } else if (arr[0].equals("RIGHT")) {
            m_world.moveRight(playerName);
        } else if (arr[0].equals("NOSPIN")) {
            m_world.stopSpinning(playerName);
        }
    }

    private class ServerGameLoop implements Runnable
    {
        public void run()
        {
            for (;;)
            {
                try {
                    Thread.sleep(45);
                } catch (InterruptedException ie) {
                    System.out.println("ServerGameLoop: interrupted!");
                    return;
                }

                /* apply changes from clients */
                synchronized (m_clientUpdates)
                {
                    for (ClientUpdate cu : m_clientUpdates)
                    {
                        processClientUpdate(cu);
                    }
                    m_clientUpdates.clear();
                }

                /* step the BlobWars world */
                m_world.step();

                /* calculated updated players */
                HashMap<String, Player> pushPlayers =
                    new HashMap<String, Player>();
                HashMap<String, Player> players = m_world.getPlayers();
                if (m_lastPlayers == null)
                    pushPlayers.putAll(m_world.getPlayers());
                else
                {
                    for (String name : players.keySet())
                    {
                        if (!m_lastPlayers.containsKey(name))
                            pushPlayers.put(name, players.get(name));
                        else if (!players.get(name).equals(
                                    m_lastPlayers.get(name)))
                            pushPlayers.put(name, players.get(name));
                    }
                }

                /* calculate updated shots */
                HashMap<Integer, Shot> pushShots =
                    new HashMap<Integer, Shot>();
                HashMap<Integer, Shot> shots = m_world.getShots();
                if (m_lastShots == null)
                    pushShots.putAll(m_world.getShots());
                else
                {
                    for (Integer shotid : shots.keySet())
                    {
                        if (!m_lastShots.containsKey(shotid))
                            pushShots.put(shotid, shots.get(shotid));
                        else if (!shots.get(shotid).equals(
                                    m_lastShots.get(shotid)))
                            pushShots.put(shotid, shots.get(shotid));
                    }
                }
                
                /* build up the update string */
                String sendLine = "";
                for (Player p : pushPlayers.values())
                {
                    sendLine += "PLAYER:" + p + "\n";
                }
                for (Shot s : pushShots.values())
                {
                    sendLine += "SHOT:" + s + "\n";
                }

                /* add remove messages for all players / shots that are gone */
                if (m_lastPlayers != null)
                {
                    for (String playerName : m_lastPlayers.keySet())
                    {
                        if (!players.containsKey(playerName))
                        {
                            /* player playerName has left since last update */
                            sendLine += "RMPLAYER:" + playerName + "\n";
                        }
                    }
                }
                if (m_lastShots != null)
                {
                    for (Integer id : m_lastShots.keySet())
                    {
                        if (!shots.containsKey(id))
                        {
                            /* shot has expired since last update */
                            sendLine += "RMSHOT:" + id + "\n";
                        }
                    }
                }

                /* check number of players alive */
                Player alivePlayer = null;
                int alivePlayers = 0;
                for (Player p : players.values())
                {
                    if (p.health > 0.0)
                    {
                        alivePlayers++;
                        alivePlayer = p;
                    }
                }

                /* save the player / shot data into the "last" variables */
                m_lastPlayers = PlayerClone(players);
                m_lastShots = ShotClone(shots);

                /* declare a winner if there is one and reset the players */
                if (alivePlayers == 1 && alivePlayers < m_lastNumPlayersAlive)
                {
                    /* game is over, alivePlayer won */
                    sendLine += "WINNER:" + alivePlayer.name + "\n";
                    m_world.resetPlayers();
                }
                m_lastNumPlayersAlive = alivePlayers;

                /* write updated info to each client */
                synchronized (m_socketToPlayerName)
                {
                    for (Socket client : m_socketToPlayerName.keySet())
                    {
                        try {
                            BufferedWriter bw = new BufferedWriter(
                                    new OutputStreamWriter(
                                        client.getOutputStream()));
                            bw.write(sendLine, 0, sendLine.length());
                            bw.flush();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }

            }
        }
    }

    private class ClientHandler implements Runnable
    {
        private Socket m_socket;

        public ClientHandler(Socket socket)
        {
            m_socket = socket;
            System.out.println("Incoming connection from " +
                m_socket.getInetAddress().getHostAddress());
        }

        public void run()
        {
            BufferedReader br;
            try {
                br = new BufferedReader(
                    new InputStreamReader(m_socket.getInputStream()));
            } catch (IOException ioe) {
                ioe.printStackTrace();
                return;
            }

            for (;;)
            {
                String line;
                try {
                    line = br.readLine();
                } catch (Exception e) {
//                    e.printStackTrace();
                    break;
                }
                if (line == null)
                    break;
//                System.out.println("SERVER GOT LINE: " + line);
                if (line.equals("QUIT"))
                    break;
                ClientUpdate cu = new ClientUpdate();
                cu.socket = m_socket;
                cu.message = line;
                synchronized (m_clientUpdates)
                {
                    m_clientUpdates.add(cu);
                }
            }

            /* end of client handling loop, remove client from world */
            synchronized (m_socketToPlayerName)
            {
                if (m_socketToPlayerName.containsKey(m_socket))
                {
                    System.out.println("Player '" +
                            m_socketToPlayerName.get(m_socket) +
                            "' signed off.");
                    m_world.removePlayer(m_socketToPlayerName.get(m_socket));
                    m_socketToPlayerName.remove(m_socket);
                }
                try {
                    m_socket.close();
                } catch (Exception e) {}
            }
        }
    }

    public HashMap<String, Player> PlayerClone(HashMap<String, Player> orig)
    {
        HashMap<String, Player> newhm = new HashMap<String, Player>();
        for (String name : orig.keySet())
        {
            Player p = orig.get(name).clone();
            newhm.put(name, p);
        }
        return newhm;
    }

    public HashMap<Integer, Shot> ShotClone(HashMap<Integer, Shot> orig)
    {
        HashMap<Integer, Shot> newhm = new HashMap<Integer, Shot>();
        for (Integer id : orig.keySet())
        {
            Shot s = orig.get(id).clone();
            newhm.put(id, s);
        }
        return newhm;
    }
}
