/**
   * Called when a team's score changes.
   *
   * @param teamID the team id
   * @param teamScore the team score
   */
  public void onTeamScoreChanged(int teamID, int teamScore) {

    // System.out.println("server: onTeamScoreChanged");

    synchronized (getNetworkPlayers()) {
      for (NetworkPlayer networkPlayer : getNetworkPlayers()) {

        try {

          DataOutputStream out = networkPlayer.getOutStream();

          synchronized (out) {

            // Message type
            out.writeInt(NetworkConstants.S2C.TEAM_SCORE_CHANGE.ordinal());
            // Team ID
            out.writeInt(teamID);
            // Team score
            out.writeInt(teamScore);
          }

        } catch (IOException e) {

          removeNetworkPlayer(networkPlayer);
          continue;
        }
      }
    }
  }
  /**
   * Called when a client disconnects - removes the player from the server
   *
   * @param socket the socket associated with the disconnected player
   */
  public void onSocketDisconnect(Socket socket) {

    for (NetworkPlayer networkPlayer : this.getNetworkPlayers()) {

      if (networkPlayer.getSocket() == socket) {

        removeNetworkPlayer(networkPlayer);
        break;
      }
    }
  }
  /**
   * Gets the network player associated with a socket
   *
   * @param socket the socket
   * @return the network player
   */
  public NetworkPlayer socketToNetworkPlayer(Socket socket) {

    for (NetworkPlayer networkPlayer : this.networkPlayers) {

      if (socket == networkPlayer.getSocket()) {

        return networkPlayer;
      }
    }

    return null;
  }
  /** This method is called to update the server world */
  @Override
  public void update(int delta) {

    // Remove network players that are marked to be deleted and add new ones
    updateNetworkPlayers();

    // Do physics
    physics.update(delta);

    // Update entities
    synchronized (this.getEntities()) {
      for (Entity entity : this.getEntities()) {

        entity.update(delta);
      }
    }

    // Do networking
    synchronized (this.networkPlayers) {
      for (NetworkPlayer networkPlayer : this.networkPlayers) {
        Socket socket = networkPlayer.getSocket();

        if (socket == null) continue;

        try {

          broadcastServerTime(networkPlayer);

          if (this.deadEntities.size() > 0) broadcastDeadEntities(networkPlayer);

          if (this.newEntities.size() > 0) broadcastNewEntities(networkPlayer);

          if (this.entityFieldsToBroadcast.size() > 0) broadcastEntityUpdates(networkPlayer);

        } catch (IOException e) {

          e.printStackTrace();

          removeNetworkPlayer(networkPlayer);
        }
      }
    }

    // Remove entities that are marked to be deleted
    deleteDeadEntities();

    // this.deadEntities.clear();
    this.newEntities.clear();
    this.entityFieldsToBroadcast.clear();
  }
  /**
   * Sends the new entity information to the network player
   *
   * @param networkPlayer the player being contacted
   * @throws IOException if there is an error writing to the output stream
   */
  private void broadcastNewEntities(NetworkPlayer networkPlayer) throws IOException {

    System.out.println("sending new entities");

    DataOutputStream out = networkPlayer.getOutStream();

    synchronized (out) {

      // Message type
      out.writeInt(NetworkConstants.S2C.CREATE_ENTITIES.ordinal());

      synchronized (this.newEntities) {

        // Number of entities
        out.writeInt(this.newEntities.size());

        for (Entity entity : this.newEntities) {

          // entity class name (without shared.entities. prefix)
          out.writeUTF(entity.getClass().getSimpleName());
          // entity properties
          entity.writeToNetStream(out);
        }
      }
    }
  }
  /**
   * Called when there is a change in mouse position
   *
   * @param socket the socket where the change originated
   * @throws IOException if there was an error reading from the socket
   */
  public void onMousePosition(Socket socket) throws IOException {

    NetworkPlayer networkPlayer = socketToNetworkPlayer(socket);
    Player networkPlayerEntity = networkPlayer.getPlayerEntity();

    DataInputStream in = new DataInputStream(socket.getInputStream());

    float mouseX = in.readFloat();
    float mouseY = in.readFloat();
    Vector2f mouseMapPos = new Vector2f(mouseX, mouseY);

    networkPlayerEntity.calculateAimVec(mouseMapPos, this);

    // System.out.println("onMousePosition: " + networkPlayer.getPlayerEntity().getID() + "," +
    // mouseMapPos);

  }
  /**
   * Sends the server time to the network player
   *
   * @param networkPlayer the player being contacted
   * @throws IOException if there is an error writing to the output stream
   */
  private void broadcastServerTime(NetworkPlayer networkPlayer) throws IOException {

    DataOutputStream out = networkPlayer.getOutStream();

    synchronized (out) {

      // Message type
      out.writeInt(NetworkConstants.S2C.SET_SERVER_TIME.ordinal());
      // Server time
      out.writeLong(getTime());
    }
  }
  /** Closes all conections and shuts down the server */
  public void shutdown() {

    System.out.println("shutdown server");

    for (NetworkPlayer networkPlayer : this.getNetworkPlayers()) {

      Socket s = networkPlayer.getSocket();

      if (s == null) continue;

      try {

        s.close();

      } catch (IOException e) {

        e.printStackTrace();
      }
    }

    if (clientAcceptor != null) clientAcceptor.shutdown();
  }
  /**
   * Updates network players list. Adds newly connected players and removes disconnected players.
   */
  private void updateNetworkPlayers() {

    synchronized (this.networkPlayers) {
      for (NetworkPlayer networkPlayer : this.newNetworkPlayers) {

        this.networkPlayers.add(networkPlayer);
      }

      for (NetworkPlayer networkPlayer : this.deadNetworkPlayers) {

        // Remove player from team list
        Team playerTeam = networkPlayer.getPlayerEntity().getTeam();
        if (playerTeam != null) playerTeam.removePlayer(networkPlayer.getPlayerEntity());

        // Remove from network players list
        this.networkPlayers.remove(networkPlayer);
      }
    }

    this.newNetworkPlayers.clear();
    this.deadNetworkPlayers.clear();
  }
  /**
   * Sends the entity update information to the network player
   *
   * @param networkPlayer the player being contacted
   * @throws IOException if there is an error writing to the output stream
   */
  private void broadcastEntityUpdates(NetworkPlayer networkPlayer) throws IOException {

    DataOutputStream out = networkPlayer.getOutStream();

    synchronized (out) {
      synchronized (this.entityFieldsToBroadcast) {
        for (NetworkedEntityField entityField : this.entityFieldsToBroadcast) {

          // Message type
          out.writeInt(NetworkConstants.S2C.UPDATE_ENTITY_FIELD.ordinal());
          // Entity ID
          out.writeInt(entityField.getParentEntityID());
          // Field ID
          out.writeInt(entityField.getFieldID());
          // Field value
          entityField.writeToNetStream(out);
        }
      }
    }
  }
  /**
   * Sends the information of dead entities to the network player
   *
   * @param networkPlayer the player being contacted
   * @throws IOException if there is an error writing to the output stream
   */
  private void broadcastDeadEntities(NetworkPlayer networkPlayer) throws IOException {

    DataOutputStream out = networkPlayer.getOutStream();

    synchronized (out) {

      // Message type
      out.writeInt(NetworkConstants.S2C.DELETE_ENTITIES.ordinal());

      synchronized (this.deadEntities) {

        // Number of entities
        out.writeInt(this.deadEntities.size());

        for (Entity entity : this.deadEntities) {

          // entity id
          out.writeInt(entity.getID());
        }
      }
    }
  }
  /**
   * Called when a client fires their gun.
   *
   * @param socket client socket
   * @throws IOException if there was an error reading from the socket
   */
  public void onGunShotInfo(Socket socket) throws IOException {

    NetworkPlayer networkPlayer = socketToNetworkPlayer(socket);
    Player networkPlayerEntity = networkPlayer.getPlayerEntity();

    DataInputStream in = new DataInputStream(socket.getInputStream());

    // Current mouse position in map coordinates
    float mouseX = in.readFloat();
    float mouseY = in.readFloat();
    Vector2f mouseMapPos = new Vector2f(mouseX, mouseY);

    // List of intersection info
    int intersectionCount = in.readInt();
    ArrayList<EntityIntersectionInfo> intersectionInfoList = null;

    if (intersectionCount > 0) {

      intersectionInfoList = new ArrayList<EntityIntersectionInfo>(intersectionCount);

      // Read intersection info
      for (int i = 0; i < intersectionCount; i++) {
        boolean ignore = false;

        Entity hitEntity = getEntityByID(in.readInt());

        if (hitEntity == null) {
          System.out.println("Invalid intersection info received");
          ignore = true;
          // throw new IOException("Invalid intersection info received");
        }

        EntityIntersectionInfo info = new EntityIntersectionInfo(hitEntity);

        // List of intersection points
        int intersectionPointCount = in.readInt();

        for (int j = 0; j < intersectionPointCount; j++) {

          float x = in.readFloat();
          float y = in.readFloat();

          Vector2f p = new Vector2f(x, y);

          info.addIntersectionPoint(p);
        }

        if (!ignore) {
          // Store intersection info
          intersectionInfoList.add(info);
        }
      }
    }

    if (networkPlayerEntity.getIsDead()) {

      System.out.println("gun shot when player is dead");

      return;
    }

    // Network gun shot to other players
    networkPlayerEntity.calculateAimVec(mouseMapPos, this);

    // networkPlayerEntity.shoot();

    networkPlayerEntity.fireGun();
    handleGunShot(networkPlayerEntity, intersectionInfoList);
  }
  /**
   * Removes a network player from the entity list
   *
   * @param networkPlayer the player to be removed
   */
  private void removeNetworkPlayer(NetworkPlayer networkPlayer) {

    deadNetworkPlayers.add(networkPlayer);
    addDeadEntity(networkPlayer.getPlayerEntity());
  }