// SpaceClient.java
//
// the client part of the game

import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.util.Vector;
import java.util.Enumeration;
import java.lang.Math;

public class SpaceClient extends Applet implements Runnable {

  // flicker free stuff
  Image offScreenImage = null;
  Graphics offScreen = null;
  
  // constants
  public final int ACCEL = 0;
  public final int DECEL = 1;
  public final int LEFT = 2;
  public final int RIGHT = 3;
  public final int FIRE = 4;
  public final int ROT_ANGLE = 15;

  // identity
  String whoami = null;
  int tilt = 0;
  
  // network attributes
  static final int port = 31337;
  static String host;

  // window attributes
  public static final int FrameWidth = 400;
  public static final int FrameHeight = 300;

  // different limitations on the game
  public static final int capacity = 10;    // at most 10 players allowed
  public static final int bulletSpeed = 5;  // how fast the bullet goes

  // objects containers...
  protected Vector shipContainer; // holds all our ships
  protected Vector bulletContainer; // holds all our bullets
  protected Vector accounting;      // used to figure out the dead ships
  
  // thread stuff
  Thread runner;

  // keylistener stuff
  keyDown keyDownInstance = new keyDown();

  // constructor
  public SpaceClient() {
    setSize (FrameWidth, FrameHeight);

    shipContainer = new Vector (capacity);
    bulletContainer = new Vector (capacity);
    accounting = new Vector (capacity);

    // listen to user's keys
    addKeyListener (keyDownInstance);
  }


   // convert a string to an int
  private int atoi (String str) {
    Integer temp = new Integer (str);

    return temp.intValue();
  }

  // convert string to a double
  private double atod (String str) {
    Double temp = new Double (str);
    return temp.doubleValue();
  }

  // check if playerID is on our tables.
  // if they are, update the ship coordinates  (old player)
  // otherwise insert ship into container      (new player)
  // important: we must also keep track of the dead players
  // to achieve that, we add playerID to accounting vector which will be
  // compared to the shipContainer vector. every element in shipContainer
  // must also be in accounting. if that isn't so, somebody was shot down,
  // and we must update the shipContainer (by removing that ship)
  private void updatePlayer (String playerID,
                             int x, int y,
                             double[][] p,
                             double dx, double dy,
                             int theta,
                             int red, int green, int blue,
                             int score) {

    // simply add playerID to accounting vector
    accounting.addElement (playerID);
    
    for (int i = 0; i < ships(); i++) {
      Ship ship = getShip (i);
      if (ship.shipOwner().equals (playerID)) {    // update ship
        ship.moveTo (x, y);                   // update ship x,y coordinates
        ship.setMotion (dx, dy);                // update ship motion
        ship.setP (p);
        ship.setScore (score);          // update the score..
        return;
      }
    }
    addShip (new Ship (playerID,                    // add ship to container
                       x, y, 20, 20, p, dx, dy,
                       new Color (red, green, blue))); 
  }

  // check if bulletID is on our tables
  // if so, update state, otherwise insert new bullet
  private void updateBullet (String bulletID,
                             int x, int y,
                             double dx, double dy) {
   
    for (int i = 0; i < bullets(); i++) {
      Bullet bullet = getBullet (i);
      if (bullet.bulletOwner().equals (bulletID)) {
        bullet.moveTo (x, y);
        bullet.setMotion (dx, dy);
      }
    }

    addBullet (new Bullet (x, y, dx, dy, bulletID));
  }
  
  private void scanner (String input) {
    String token = null;
    String playerID, bulletID;
    int x, y;
    double[][] p = new double[3][3];
    double dx, dy;
    int theta;
    int red, green, blue;
    int score;
    Token tokenObject = new Token();

    // scan one line
    token = tokenObject.getNextToken (input);
    
    if (token.length() >= 6 && token.substring (0, 6).equals ("player")) {
      // we have a ship. unmarshal the data
      playerID = new String (token);
      x       = atoi (tokenObject.getNextToken (input));
      y       = atoi (tokenObject.getNextToken (input));
      p[0][0] = atod (tokenObject.getNextToken (input));
      p[0][1] = atod (tokenObject.getNextToken (input));
      p[1][0] = atod (tokenObject.getNextToken (input));
      p[1][1] = atod (tokenObject.getNextToken (input));
      p[2][0] = atod (tokenObject.getNextToken (input));
      p[2][1] = atod (tokenObject.getNextToken (input));
      dx      = atod (tokenObject.getNextToken (input));
      dy      = atod (tokenObject.getNextToken (input));
      theta   = atoi (tokenObject.getNextToken (input));
      red     = atoi (tokenObject.getNextToken (input));
      green   = atoi (tokenObject.getNextToken (input));
      blue    = atoi (tokenObject.getNextToken (input));
      score   = atoi (tokenObject.getNextToken (input));
      updatePlayer (playerID, x, y, p, dx, dy, theta, red, green, blue, score);
    }
    else if (token.length() >= 6 && token.substring (0, 6).equals ("bullet")) {
      // unmarshall bullets
      bulletID = new String (token);
      x       = atoi (tokenObject.getNextToken (input));
      y       = atoi (tokenObject.getNextToken (input)); 
      dx      = atod (tokenObject.getNextToken (input));
      dy      = atod (tokenObject.getNextToken (input));
      updateBullet (bulletID, x, y, dx, dy);
    }
    else if (token.equals ("EOT") || token.equals ("(null)"))
      return;
  }
 

  private void removeTheDead() {
    boolean found;
    Ship ship1;
    Ship ship2;

    if (accounting.isEmpty())
      rmAllShips();
    else {
      if (!accounting.isEmpty()) {
        for (int i = 0; i < ships(); i++) {
          found = false;
          for (int j = 0; j < accounting.size(); j++) {
            String a = new String ( (String) accounting.elementAt (j));
            Ship ship = getShip (i);
            String b = new String (ship.shipOwner());
            if (b.equals (a)) {
              accounting.removeElementAt (j);
              found = true;
              j = accounting.size();
            }
          }
          if (!found)                            // delete the dead ship
            rmShip(i);
        }
      }
    }
  }
  
  
  public void start()  {
    if (runner == null) {
      runner = new Thread (this);
      runner.start();
    }
  }

  // keep connecting to the server and updating the state
  public void run()  {
    String input = null;
    Socket socket = null;
    PrintWriter out = null;
    BufferedReader in = null;
    
    while (true) {
      try {
        socket = new Socket (host, port);
        out = new PrintWriter (socket.getOutputStream(), true);
        in = new BufferedReader
          (new InputStreamReader (socket.getInputStream()));

        out.println ("GET;");
        input = in.readLine();
        delay (20);
        if (input.equals ("EOT"))    // no ships!!! take care of accounting
          accounting.removeAllElements();
 
        while (!input.equals ("EOT")) {
          scanner (input);                 // update everybody
          input = in.readLine();
        }

        // now we must figure out who the dead ships are
        removeTheDead();
        
        out.close();
        in.close();
        socket.close();
      } catch (UnknownHostException e) {
        System.err.println ("Don't know about host: " + host);
        System.exit(1);
      }
      catch (IOException e) {
        System.err.println("Couldn't get I/O for the connection to:" + host);
        System.exit(1);
      }
      delay (120); 
    }
  }
  
  public void stop() {
    if (runner != null) {
      runner.stop();
      runner = null;
    }
  }

  // randomize initial values for ship and create the command
  // that the server could understand
  private String cmdGenerator() {
    // first generate random coordinates and vectors...
    int x = (int) (Math.random() * size().width);
    int y = (int) (Math.random() * size().height);

    // generate random color for the ship 0..9
    int color = (int) (Math.random() * 9);

    // generate random user id in the form playerXXXX
    String playerID = new String();
    playerID = "player" +
      (int) (Math.random() * 9) +
      (int) (Math.random() * 9) +
      (int) (Math.random() * 9) +
      (int) (Math.random() * 9);

    whoami = new String (playerID);
    
    // now generate the string
    String cmd = new String ("ADD" + " " + playerID + " " +
                            x + " " + y + " " + color + ";");

    return cmd;
  }

  // register ourselves with the server
  private void register() {
    String input;
    Socket socket = null;
    PrintWriter out = null;
    BufferedReader in = null;    
    try {
      socket = new Socket (host, port);
      out = new PrintWriter (socket.getOutputStream(), true);
      in = new BufferedReader
        (new InputStreamReader (socket.getInputStream()));
      
      out.println (cmdGenerator());  
      input = in.readLine();

      if (input.length() > 0 && input.substring (0, 1).equals("+"))
        System.err.println ("successfully registered our ship w/ server");
      else
        System.err.println ("unable to register ship with server");
      out.close();
      in.close();
      socket.close();
    } catch (UnknownHostException e) {
      System.err.println ("Don't know about host: " + host);
      System.exit(1);
    }
    catch (IOException e) {
      System.err.println("Couldn't get I/O for the connection to:" + host);
      System.exit(1);
    }
  }

  // initialize our app
  public void init()  {
    host = new String (getCodeBase().getHost());
    System.out.println ("server is running on " + host);

    // set up anti-flicker shit
    offScreenImage = createImage (FrameWidth, FrameHeight);
    offScreen = offScreenImage.getGraphics ();
    
    // instantiate ourselves..
    SpaceClient client = new SpaceClient();

    // register ourselves with the server
    register();
    
    // show ourselves to the world
    client.show();
  }

  // when we quit, we simply notify server to DELETE us...
  public void quit() {
    Socket socket = null;
    PrintWriter out = null;
    BufferedReader in = null;
    String cmd = new String ("DEL" + " " + whoami + ";");
    String input;
    
    try {
      socket = new Socket (host, port);
      out = new PrintWriter (socket.getOutputStream(), true);
      in = new BufferedReader
        (new InputStreamReader (socket.getInputStream()));
      
      out.println (cmd);
      input = in.readLine();

      if (input == null)
        System.err.println ("tried to disconnect, server did not reply");
      else if (!input.substring (0, 1).equals ("+"))
        System.err.println ("got reply but quit was not ACKed");
      else
        System.err.println ("successfully logged off from the game");

      in.close();
      out.close();
      socket.close();
    } catch (UnknownHostException e) {
      System.err.println ("Don't know about host: " + host);
      System.exit(1);
    }
    catch (IOException e) {
      System.err.println("Couldn't get I/O for the connection to:" + host);
      System.exit(1);
    }

    // unload keylistener
    removeKeyListener (keyDownInstance);
    
    System.err.println ("bye..........");
  }

  // if user pressed some button to control the ship, tell the server
  private void updateServer (int action) {
    Ship ship = getShip (whoami);
    String cmd = null;
    String input = null;
    double[][] p = new double[3][3];
    double dxOld = ship.xMotion();
    double dyOld = ship.yMotion();
    int xmid, ymid, x, y, dxNew, dyNew;

    if (ship == null) return;
    
    switch (action) {
    case FIRE:
      // build up the command
      cmd = new String
        ("UPD" + " " +
         ship.shipOwner() + " " +
         (int) dxOld + " " +
         (int) dyOld + " " +
         0 + " " +
         "y"   + ";");     // do nothing... let server figure out bullet
      break;
    case ACCEL:
      p = ship.dimensions();
      xmid = (int) ( ((p[0][0]) +  (p[2][0])) / 2 );
      ymid = (int) ( ((p[0][1]) +  (p[2][1])) / 2 );
      x =  (int) p[1][0];
      y =  (int) p[1][1];
      dxNew = (int) (((x - xmid) / 8) + dxOld);
      dyNew = (int) (((y - ymid) / 8) + dyOld);
      // build up the command
      cmd = new String
        ("UPD" + " " +
         ship.shipOwner() + " " +
         dxNew + " " +
         dyNew + " " +
         0 + " " +
         "n"   + ";");     // just accelerate, no bullets...
      break;
    case DECEL:
      p = ship.dimensions();
      xmid = (int) ( ((p[0][0]) +  (p[2][0])) / 2 );
      ymid = (int) ( ((p[0][1]) +  (p[2][1])) / 2 );
      x =  (int) p[1][0];
      y =  (int) p[1][1];
      dxNew = (int) (-((x - xmid) / 8) + dxOld);
      dyNew = (int) (-((y - ymid) / 8) + dyOld);
      // build up the command
      cmd = new String
        ("UPD" + " " +
         ship.shipOwner() + " " +
         dxNew + " " +
         dyNew + " " +
         0 + " " +
         "n"   + ";");     // just decelerate, no bullets...
      break;
    case LEFT:
      tilt = ROT_ANGLE;
      // build up the command
      cmd = new String
        ("UPD" + " " +
         ship.shipOwner() + " " +
         (int) dxOld + " " +
         (int) dyOld + " " +
         tilt + " " +     // just change theta/tilt/angle/whatever!
         "n"   + ";");     
      break;
    case RIGHT:
      tilt = -ROT_ANGLE;
      // build up the command
      cmd = new String
        ("UPD" + " " +
         ship.shipOwner() + " " +
         (int) dxOld + " " +
         (int) dyOld + " " +
         tilt + " " +     // just change theta/tilt/angle/whatever!
         "n"   + ";");     
      break;
    }

    // send the command to the server
    // notice that we do not even bother to update ourselves...
    // we let our run() thread update ourselves with the information
    // from the server. recall that the server is the authority...all clients
    // synchronize with it
    
    Socket socket = null;
    PrintWriter out = null;
    BufferedReader in = null;
    try {
      socket = new Socket (host, port);
      out = new PrintWriter (socket.getOutputStream(), true);
      in = new BufferedReader
        (new InputStreamReader (socket.getInputStream()));     
      out.println (cmd);
      input = in.readLine();
      if (input == null)
        System.err.println ("lost sync with server!!");
      else if (input.substring (0, 1).equals ("+"))
        System.err.println ("updated successfully");
      else if (input.substring (0, 1).equals ("-"))
        System.err.println ("NACK on update!!");
      else
        System.err.println ("server responded in an ambiguous way");
      out.close();
      socket.close();
    } catch (UnknownHostException e) {
      System.err.println ("Don't know about host: " + host);
    }
    catch (IOException e) {
      System.err.println("non critical: no i/o for the connection to:" + host);
    }
  }
  
  private class keyDown extends KeyAdapter {
    public void keyPressed (KeyEvent e) {
      char key = e.getKeyChar();
      switch (key) {
      case 'i' :
      case 'I' :
        updateServer (ACCEL);
        System.err.println ("accelerate");
        break;
      case 'm' :
      case 'M' :
        updateServer (DECEL);
        System.err.println ("slow down");
        break;
      case 'j' :
      case 'J' :
        updateServer (LEFT);
        System.err.println ("left");
        break;
      case 'l' :
      case 'L' :
        updateServer (RIGHT);
        System.err.println ("right");
        break;
      case ' ' :
        updateServer (FIRE);
        System.err.println ("fire");
        break;
      case 'q' :
      case 'Q' : 
        quit();
        System.err.println ("quit");
        break;
      }
    }
  }

  // returns the # of ships in the container
  protected int ships () { return shipContainer.size(); }

  // return the # of bullets in the container
  protected int bullets () { return bulletContainer.size(); }

  // gets ship at position i, keeping it in the vector
  protected Ship getShip (int i) { return (Ship) shipContainer.elementAt (i); }

  // gets ship based on player name
  protected Ship getShip (String playerID) {
    if (playerID == null)
      return null;
    
    Ship ship;
    for (int i = 0; i < ships(); i++) {
      ship = getShip (i);
      if (ship.shipOwner().equals (playerID))
        return ship;
    }
    return null;
  }
    
  // gets bullet at position i, keeping it in the vector
  protected Bullet getBullet (int i) {
    return (Bullet) bulletContainer.elementAt (i);
  }

  // adds ship into the container
  protected void addShip (Ship ship) { shipContainer.addElement (ship); }

  // adds bullet into the container
  protected void addBullet (Bullet bullet) {
    bulletContainer.addElement (bullet);
  }

  // removes ship from container, at position pos, destroying it
  protected void rmShip (int pos) { shipContainer.removeElementAt (pos); }

  // remove all the ships...
  protected void rmAllShips() { shipContainer.removeAllElements(); }
  
  // remove bullet from container, at position pos, destroying it
  protected void rmBullet (int pos) { bulletContainer.removeElementAt (pos); }

  // a fix for the modulo operation to handle negative numbers
  // ex/  -3 % 10 = 7. assume b > 0
  private int mod (int a, int b) {
    if (a >= 0)
      return a % b;
    else 
      return b - (Math.abs (a) % b);
  }

  public void update(Graphics g) {
    paint(g);
  }
  
  public void paint (Graphics g) {
    offScreen.setColor (getBackground());
    offScreen.fillRect (0, 0, FrameWidth, FrameHeight);
    offScreen.setColor (Color.black);

    // display the client's score on the screen
    Ship me = getShip (whoami);
    String s;

    if (me != null)
      s = "score: " + me.getScore();
    else
      s = "score: n/a";
    
    offScreen.drawString ( s, 10, 10); 

    Ship ship;
    Bullet bullet;
    
    for (int i = 0; i < ships(); i++) {
      ship = getShip (i);
      ship.draw (offScreen);
      ship.move();                             // move the ship..
      
      ship.moveTo (mod (ship.x(), size().width),
                   mod (ship.y(), size().height)); // screen wrapping
    }

    for (int i = 0; i < bullets(); i++) {
      bullet = getBullet (i);
      bullet.draw (offScreen);
      bullet.move();

      if (bullet.x() < 0 ||
          bullet.y() < 0 ||
          bullet.x() > size().width ||
          bullet.y() > size().height)
        rmBullet (i);                      // bullet off screen --> deleted
    }

    g.drawImage (offScreenImage, 0, 0, this);
    delay (150);
    repaint();    
  }

  private void delay (int millis) {
    try { Thread.sleep (millis); } catch (Exception ignored) {}
  }
}
