// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.components.runtime;

import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.collect.Lists;
import com.google.appinventor.components.runtime.errors.YailRuntimeError;
import com.google.appinventor.components.runtime.util.AsyncCallbackPair;
import com.google.appinventor.components.runtime.util.AsynchUtil;
import com.google.appinventor.components.runtime.util.GameInstance;
import com.google.appinventor.components.runtime.util.JsonUtil;
import com.google.appinventor.components.runtime.util.PlayerListDelta;
import com.google.appinventor.components.runtime.util.WebServiceUtil;
import com.google.appinventor.components.runtime.util.YailList;

import android.app.Activity;
import android.os.Handler;
import android.util.Log;

import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;

/**
 * GameClient provides a way for AppInventor applications to
 * communicate with online game servers. This allows users to create
 * games that are coordinated and managed in the cloud.
 *
 * Most communication is done by sending keyed messages back and
 * forth between the client and the server in the form of YailLists.
 * The server and game client can then switch on the keys and perform
 * more complex operations on the data. In addition, game servers can
 * implement a library of server commands that can perform complex
 * functions on the server and send back responses that are converted
 * into YailLists and sent back to the component. For more
 * information about server commands, consult the game server code
 * at http://code.google.com/p/app-inventor-for-android/
 *
 * Games instances are uniquely determined by a game id and an
 * instance id. In general, each App Inventor program should have
 * its own game id. Then, when running different instances of that
 * program, new instance ides should be used. Players are
 * represented uniquely by the email address registered to their
 * phones.
 *
 * All call functions perform POSTs to a web server. Upon successful
 * completion of these POST requests, FunctionCompleted will be
 * triggered with the function name as an argument. If the post
 * fails, WebServiceError will trigger with the function name and the
 * error message as arguments. These calls allow for application
 * creators to deal with web service failures and keep track of the
 * success or failure of their operations. The only exception to this
 * is when the return value from the server has the incorrect game id
 * or instance id. In this case, the response is completely ignored
 * and neither of these events will trigger.
 *
 *
 */
@DesignerComponent(version = YaVersion.GAMECLIENT_COMPONENT_VERSION,
    description = "Provides a way for applications to communicate with online game servers",
    category = ComponentCategory.INTERNAL, // moved to internal until fully tested
    nonVisible = true,
    iconName = "images/gameClient.png")
@SimpleObject
@UsesPermissions(
    permissionNames = "android.permission.INTERNET, " +
                "com.google.android.googleapps.permission.GOOGLE_AUTH")
public class GameClient extends AndroidNonvisibleComponent
    implements Component, OnResumeListener, OnStopListener {

  private static final String LOG_TAG = "GameClient";

  // Parameter keys
  private static final String GAME_ID_KEY = "gid";
  private static final String INSTANCE_ID_KEY = "iid";
  private static final String PLAYER_ID_KEY = "pid";
  private static final String INVITEE_KEY = "inv";
  private static final String LEADER_KEY = "leader";
  private static final String COUNT_KEY = "count";
  private static final String TYPE_KEY = "type";
  private static final String INSTANCE_PUBLIC_KEY = "makepublic";
  private static final String MESSAGE_RECIPIENTS_KEY = "mrec";
  private static final String MESSAGE_CONTENT_KEY = "contents";
  private static final String MESSAGE_TIME_KEY = "mtime";
  private static final String MESSAGE_SENDER_KEY = "msender";
  private static final String COMMAND_TYPE_KEY = "command";
  private static final String COMMAND_ARGUMENTS_KEY = "args";
  private static final String SERVER_RETURN_VALUE_KEY = "response";
  private static final String MESSAGES_LIST_KEY = "messages";
  private static final String ERROR_RESPONSE_KEY = "e";
  private static final String PUBLIC_LIST_KEY = "public";
  private static final String JOINED_LIST_KEY = "joined";
  private static final String INVITED_LIST_KEY = "invited";
  private static final String PLAYERS_LIST_KEY = "players";

  // Command keys
  private static final String GET_INSTANCE_LISTS_COMMAND = "getinstancelists";
  private static final String GET_MESSAGES_COMMAND = "messages";
  private static final String INVITE_COMMAND = "invite";
  private static final String JOIN_INSTANCE_COMMAND = "joininstance";
  private static final String LEAVE_INSTANCE_COMMAND = "leaveinstance";
  private static final String NEW_INSTANCE_COMMAND = "newinstance";
  private static final String NEW_MESSAGE_COMMAND = "newmessage";
  private static final String SERVER_COMMAND = "servercommand";
  private static final String SET_LEADER_COMMAND = "setleader";

  // URL for accessing the game server
  private String serviceUrl;
  private String gameId;
  private GameInstance instance;
  private Handler androidUIHandler;
  private Activity activityContext;

  private String userEmailAddress = "";

  // Game instances in the current GameId that this player has joined
  private List<String> joinedInstances;
  // Game instances to which this player has been invited
  private List<String> invitedInstances;
  // Game instances which have been made public.
  private List<String> publicInstances;

  /**
   * Creates a new GameClient component.
   *
   * @param container the Form that this component is contained in.
   */
  public GameClient(ComponentContainer container) {
    super(container.$form());
    // Note that although this is creating a new Handler there is
    // only one UI thread in an Android app and posting to this
    // handler queues up a Runnable for execution on that thread.
    androidUIHandler = new Handler();
    activityContext = container.$context();
    form.registerForOnResume(this);
    form.registerForOnStop(this);
    gameId = "";
    instance = new GameInstance("");
    joinedInstances = Lists.newArrayList();
    invitedInstances = Lists.newArrayList();
    publicInstances = Lists.newArrayList();
    serviceUrl = "http://appinvgameserver.appspot.com";

    // This needs to be done in a separate thread since it uses
    // a blocking service to complete and will cause the UI to hang
    // if it happens in the constructor.
    /*
     * Remove this code until we fix LoginServiceUtil to work in later
     * versions of the android SDK.
    AsynchUtil.runAsynchronously(new Runnable() {
      @Override
      public void run() {
        userEmailAddress = LoginServiceUtil.getPhoneEmailAddress(activityContext);
        if (!userEmailAddress.equals("")) {
          UserEmailAddressSet(userEmailAddress);
        }
      }
    });
    */
  }


  //----------------------------------------------------------------
  // Properties

  /**
   * Returns a string indicating the game name for this application.
   * The same game ID can have one or more game instances.
   */
  @SimpleProperty(
      description = "The game name for this application. " +
      "The same game ID can have one or more game instances.",
      category = PropertyCategory.BEHAVIOR)
  public String GameId() {
    return gameId;
  }

  /**
   * Specifies a string indicating the family of the current game
   * instance.  The same game ID can have one or more game instance
   * IDs.
   */
  // Only exposed in the designer to enforce that each GameClient
  // instance should be made for a single GameId.
  @DesignerProperty(
      editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING,
      defaultValue = "")
  public void GameId(String id) {
    this.gameId = id;
  }

  /**
   * Returns the game instance id.  Taken together, the game ID and
   * the instance ID uniquely identify the game.
   */
  @SimpleProperty(
      description = "The game instance id.  Taken together," +
      "the game ID and the instance ID uniquely identify the game.",
      category = PropertyCategory.BEHAVIOR)
  public String InstanceId() {
    return instance.getInstanceId();
  }

  /**
   * Returns the set of game instances to which this player has been
   * invited but has not yet joined.  To ensure current values are
   * returned, first invoke {@link #GetInstanceLists}.
   */
  @SimpleProperty(
      description = "The set of game instances to which this player has been " +
                    "invited but has not yet joined.  To ensure current values are " +
                    "returned, first invoke GetInstanceLists.",
      category = PropertyCategory.BEHAVIOR)
  public List<String> InvitedInstances() {
    return invitedInstances;
  }

  /**
   * Returns the set of game instances in which this player is
   * participating.  To ensure current values are returned, first
   * invoke {@link #GetInstanceLists}.
   */
  @SimpleProperty(
      description = "The set of game instances in which this player is " +
      "participating.  To ensure current values are returned, first " +
      "invoke GetInstanceLists.",
      category = PropertyCategory.BEHAVIOR)
  public List<String> JoinedInstances() {
    return joinedInstances;
  }

  /**
   * Returns the game's leader. At any time, each game instance has
   * only one leader, but the leader may change with time.
   * Initially, the leader is the game instance creator. Application
   * writers determine special properties of the leader. The leader
   * value is updated each time a successful communication is made
   * with the server.
   */
  @SimpleProperty(
      description = "The game's leader. At any time, each game instance has " +
      "only one leader, but the leader may change with time.  " +
      "Initially, the leader is the game instance creator. Application " +
      "writers determine special properties of the leader. The leader " +
      "value is updated each time a successful communication is made " +
      "with the server.",
      category = PropertyCategory.BEHAVIOR)
  public String Leader() {
    return instance.getLeader();
  }

  /**
   * Returns the current set of players for this game instance. Each
   * player is designated by an email address, which is a string. The
   * list of players is updated each time a successful communication
   * is made with the game server.
   */
  @SimpleProperty(
      description = "The current set of players for this game instance. Each " +
      "player is designated by an email address, which is a string. The " +
      "list of players is updated each time a successful communication " +
      "is made with the game server.",
      category = PropertyCategory.BEHAVIOR)
  public List<String> Players() {
    return instance.getPlayers();
  }

  /**
   * Returns the set of game instances that have been marked public.
   * To ensure current values are returned, first
   * invoke {@link #GetInstanceLists}.
   */
  @SimpleProperty(
      description = "The set of game instances that have been marked public. " +
      "To ensure current values are returned, first " +
      "invoke {@link #GetInstanceLists}. ",
      category = PropertyCategory.BEHAVIOR)
  public List<String> PublicInstances() {
    return publicInstances;
  }

  /**
   * The URL of the game server.
   */
  @SimpleProperty(
      description = "The URL of the game server.",
      category = PropertyCategory.BEHAVIOR)
  public String ServiceUrl() {
    return serviceUrl;
  }

  /**
   * Set the URL of the game server.
   *
   * @param url The URL (include initial http://).
   */
  @DesignerProperty(
      editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING,
      defaultValue = "http://appinvgameserver.appspot.com")
  @SimpleProperty(userVisible = false)
  public void ServiceURL(String url){
    if (url.endsWith("/")) {
      this.serviceUrl = url.substring(0, url.length() - 1);
    } else {
      this.serviceUrl = url;
    }
  }

  /**
   * Returns the registered email address that is being used as the
   * player id for this game client.
   */
  @SimpleProperty(
      description = "The email address that is being used as the " +
                    "player id for this game client.   At present, users " +
                    "must set this manually in oder to join a game.  But " +
                    "this property will change in the future so that is set " +
      "automatically, and users will not be able to change it.",
      category = PropertyCategory.BEHAVIOR)

  public String UserEmailAddress() {
    if (userEmailAddress.equals("")) {
      Info("User email address is empty.");
    }
    return userEmailAddress;
  }

  /**
   * Changes the player of this game by changing the email address
   * used to communicate with the server.
   *
   * This should only be used during development. Games should not
   * allow players to set their own email address.
   *
   * @param emailAddress The email address to set the current player
   * id to.
   */
  @SimpleProperty
  public void UserEmailAddress(String emailAddress) {
    userEmailAddress = emailAddress;
    UserEmailAddressSet(emailAddress);
  }

  //----------------------------------------------------------------
  // Event Handlers

  /**
   * Indicates that a server request from a function call has
   * completed. This can be used to control a polling loop or
   * otherwise respond to server request completions.
   *
   * @param functionName The name of the App Inventor function that
   * finished.
   */
  @SimpleEvent(description = "Indicates that a function call completed.")
  public void FunctionCompleted(final String functionName) {
    androidUIHandler.post(new Runnable() {
      public void run() {
        Log.d(LOG_TAG, "Request completed: " + functionName);
        EventDispatcher.dispatchEvent(GameClient.this, "FunctionCompleted", functionName);
      }});
  }

  /**
   * Ensures that the GameId was set by the game creator.
   */
  public void Initialize() {
    Log.d(LOG_TAG, "Initialize");
    if (gameId.equals("")) {
      throw new YailRuntimeError("Game Id must not be empty.", "GameClient Configuration Error.");
    }
  }

  /**
   * Indicates that a GetMessages call received a message. This could
   * be invoked multiple times for a single call to GetMessages.
   *
   * @param type The type of the message received.
   * @param contents The message's contents. Consists of a list
   * nested to arbitrary depth that includes string, boolean and
   * number values.
   */
  @SimpleEvent(description = "Indicates that a new message has " +
                "been received.")
  public void GotMessage(final String type, final String sender, final List<Object> contents) {
    Log.d(LOG_TAG, "Got message of type " + type);
    androidUIHandler.post(new Runnable() {
      public void run() {
        EventDispatcher.dispatchEvent(GameClient.this, "GotMessage", type, sender, contents);
      }});
  }

  /**
   * Indicates that InstanceId has changed due to the creation of a
   * new instance or setting the InstanceId.
   *
   * @param instanceId The id of the instance the player is now in.
   */
  @SimpleEvent(description = "Indicates that the InstanceId " +
                "property has changed as a result of calling " +
                "MakeNewInstance or SetInstance.")
  public void InstanceIdChanged(final String instanceId) {
    Log.d(LOG_TAG, "Instance id changed to " + instanceId);
    androidUIHandler.post(new Runnable() {
      public void run() {
        EventDispatcher.dispatchEvent(GameClient.this, "InstanceIdChanged", instanceId);
      }});
  }

  /**
   * Indicates a user has been invited to this game instance by
   * another player.
   *
   * @param instanceId The id of the new game instance.
   */
  @SimpleEvent(
      description = "Indicates that a user has been invited to " +
                "this game instance.")
  public void Invited(final String instanceId) {
    Log.d(LOG_TAG, "Player invited to " + instanceId);
    androidUIHandler.post(new Runnable() {
      public void run() {
        EventDispatcher.dispatchEvent(GameClient.this, "Invited", instanceId);
      }});
  }

  /**
   * Indicates this game instance has a new leader. This could happen
   * in response to a call to SetLeader or by the side effects of a
   * server command performed by any player in the game.
   *
   * Since the current leader is sent back with every server
   * response, NewLeader can trigger after making any server call.
   *
   * @param playerId The email address of the new leader.
   */
  @SimpleEvent(description = "Indicates that this game has a new " +
                "leader as specified through SetLeader")
  public void NewLeader(final String playerId) {
    androidUIHandler.post(new Runnable() {
      public void run() {
        Log.d(LOG_TAG, "Leader change to " + playerId);
        EventDispatcher.dispatchEvent(GameClient.this, "NewLeader", playerId);
      }});
  }

  /**
   * Indicates this game instance was created as specified via
   * MakeNewInstance. The creating player is automatically the leader
   * of the instance and the InstanceId property has already been set
   * to this new instance.
   *
   * @param instanceId The id of the newly created game instance.
   */
  @SimpleEvent(description = "Indicates that a new instance was " +
                "successfully created after calling MakeNewInstance.")
  public void NewInstanceMade(final String instanceId) {
    androidUIHandler.post(new Runnable() {
      public void run() {
        Log.d(LOG_TAG, "New instance made: " + instanceId);
        EventDispatcher.dispatchEvent(GameClient.this, "NewInstanceMade", instanceId);
      }});
  }

  /**
   * Indicates that a player has joined this game instance.
   *
   * @param playerId The email address of the new player.
   */
  @SimpleEvent(description = "Indicates that a new player has " +
                "joined this game instance.")
  public void PlayerJoined(final String playerId) {
    androidUIHandler.post(new Runnable() {
      public void run() {
        if (!playerId.equals(UserEmailAddress())) {
          Log.d(LOG_TAG, "Player joined: " + playerId);
          EventDispatcher.dispatchEvent(GameClient.this, "PlayerJoined", playerId);
        }
      }});
  }

  /**
   * Indicates that a player has left this game instance.
   *
   * @param playerId The email address of the player that left.
   */
  @SimpleEvent(description = "Indicates that a player has left " +
                "this game instance.")
  public void PlayerLeft(final String playerId) {
    androidUIHandler.post(new Runnable() {
      public void run() {
        Log.d(LOG_TAG, "Player left: " + playerId);
        EventDispatcher.dispatchEvent(GameClient.this, "PlayerLeft", playerId);
      }});
  }

  /**
   * Indicates that an attempt to complete a server command failed on
   * the server.
   * @param command The command requested.
   * @param arguments The arguments sent to the command.
   */
  @SimpleEvent(
      description = "Indicates that a server command failed.")
  public void ServerCommandFailure(final String command, final YailList arguments) {
    androidUIHandler.post(new Runnable() {
      public void run() {
        Log.d(LOG_TAG, "Server command failed: " + command);
        EventDispatcher.dispatchEvent(GameClient.this, "ServerCommandFailure", command, arguments);
      }});
  }

  /**
   * Indicates that a ServerCommand completed.
   *
   * @param command The key for the command that resulted in this
   * response.
   * @param response The server response. This consists of a list
   * nested to arbitrary depth that includes string, boolean and
   * number values.
   */
  @SimpleEvent(description = "Indicates that a server command " +
                "returned successfully.")
  public void ServerCommandSuccess(final String command, final List<Object> response) {
    Log.d(LOG_TAG, command + " server command returned.");
    androidUIHandler.post(new Runnable() {
      public void run() {
        EventDispatcher.dispatchEvent(GameClient.this,
            "ServerCommandSuccess", command, response);
      }});
  }

  /**
   * Indicates that the user email address property has been
   * successfully set. This event should be used to initialize
   * any web service functions.
   *
   * This separate event was required because the email address was
   * unable to be first fetched from the the UI thread without
   * causing programs to hang. GameClient will now start fetching
   * the user email address in its constructor and trigger this event
   * when it finishes.
   */
  @SimpleEvent(description = "Indicates that the user email " +
                "address has been set.")
  public void UserEmailAddressSet(final String emailAddress) {
    Log.d(LOG_TAG, "Email address set.");
    androidUIHandler.post(new Runnable() {
      public void run() {
        EventDispatcher.dispatchEvent(GameClient.this, "UserEmailAddressSet", emailAddress);
      }});
  }

  //----------------------------------------------------------------
  // Message events

  /**
   * Indicates that something has occurred which the player should be
   * somehow informed of.
   *
   * @param message the message.
   */
  @SimpleEvent(description = "Indicates that something has " +
                "occurred which the player should know about.")
  public void Info(final String message) {
    Log.d(LOG_TAG, "Info: " + message);
    androidUIHandler.post(new Runnable() {
      public void run() {
        EventDispatcher.dispatchEvent(GameClient.this, "Info", message);
      }});

  }

  /**
   * Indicates that the attempt to communicate with the web service
   * resulted in an error.
   *
   * @param functionName The name of the function call that caused this
   * error.
   * @param message the error message
   */
  @SimpleEvent(description = "Indicates that an error occurred " +
                "while communicating with the web server.")
  public void WebServiceError(final String functionName, final String message) {
    Log.e(LOG_TAG, "WebServiceError: " + message);
    androidUIHandler.post(new Runnable() {
      public void run() {
        EventDispatcher.dispatchEvent(GameClient.this, "WebServiceError", functionName, message);
      }});
  }

  //----------------------------------------------------------------
  // Functions

  /**
   * Updates the current InstancesJoined and InstancesInvited lists.
   *
   * If the player has been invited to new instances an Invited
   * event will be raised for each new instance.
   */
  @SimpleFunction(description = "Updates the InstancesJoined and " +
                "InstancesInvited lists. This procedure can be called " +
                "before setting the InstanceId.")
  public void GetInstanceLists() {
    AsynchUtil.runAsynchronously(new Runnable() {
        public void run() { postGetInstanceLists(); }});
  }

  private void postGetInstanceLists() {
    AsyncCallbackPair<JSONObject> readMessagesCallback = new AsyncCallbackPair<JSONObject>(){
      public void onSuccess(final JSONObject response) {
        processInstanceLists(response);
        FunctionCompleted("GetInstanceLists");
      }
      public void onFailure(final String message) {
        WebServiceError("GetInstanceLists", "Failed to get up to date instance lists.");
      }
    };

    postCommandToGameServer(GET_INSTANCE_LISTS_COMMAND,
        Lists.<NameValuePair>newArrayList(
            new BasicNameValuePair(GAME_ID_KEY, GameId()),
            new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
            new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress())),
        readMessagesCallback);
  }

  private void processInstanceLists(JSONObject instanceLists){
    try {
      joinedInstances = JsonUtil.getStringListFromJsonArray(instanceLists.
          getJSONArray(JOINED_LIST_KEY));

      publicInstances = JsonUtil.getStringListFromJsonArray(instanceLists.
          getJSONArray(PUBLIC_LIST_KEY));

      List<String> receivedInstancesInvited = JsonUtil.getStringListFromJsonArray(instanceLists.
          getJSONArray(INVITED_LIST_KEY));

      if (!receivedInstancesInvited.equals(InvitedInstances())) {
        List<String> oldList = invitedInstances;
        invitedInstances = receivedInstancesInvited;
        List<String> newInvites = new ArrayList<String>(receivedInstancesInvited);
        newInvites.removeAll(oldList);

        for (final String instanceInvited : newInvites) {
          Invited(instanceInvited);
        }
      }

    } catch (JSONException e) {
      Log.w(LOG_TAG, e);
      Info("Instance lists failed to parse.");
    }
  }

  /**
   * Retrieves messages of the specified type.
   *
   * Requests that only messages which have not been seen during
   * the current session are returned. Messages will be processed
   * in chronological order with the oldest first, however, only
   * the count newest messages will be retrieved. This means that
   * one could "miss out" on some messages if they request less than
   * the number of messages created since the last request for
   * that message type.
   *
   * Setting type to the empty string will fetch all message types.
   * Even though those message types were not specifically requested,
   * their most recent message time will be updated. This keeps
   * players from receiving the same message again if they later
   * request the specific message type.
   *
   * Note that the message receive times are not updated until after
   * the messages are actually received. Thus, if multiple message
   * requests are made before the previous ones return, they could
   * send stale time values and thus receive the same messages more
   * than once. To avoid this, application creators should wait for
   * the get messages function to return before calling it again.
   *
   * @param type The type of message to retrieve. If the empty string
   * is used as the message type then all message types will be
   * requested.
   * @param count The maximum number of messages to retrieve. This
   * should be an integer from 1 to 1000.
   */
  @SimpleFunction(
      description = "Retrieves messages of the specified type.")
  public void GetMessages(final String type, final int count) {
    AsynchUtil.runAsynchronously(new Runnable() {
      public void run() { postGetMessages(type, count); }});
  }

  private void postGetMessages(final String requestedType, final int count) {
    AsyncCallbackPair<JSONObject> myCallback = new AsyncCallbackPair<JSONObject>() {
      public void onSuccess(final JSONObject result) {
        try {
          int count = result.getInt(COUNT_KEY);
          JSONArray messages = result.getJSONArray(MESSAGES_LIST_KEY);
          for (int i = 0; i < count; i++) {
            JSONObject message = messages.getJSONObject(i);
            String type = message.getString(TYPE_KEY);
            String sender = message.getString(MESSAGE_SENDER_KEY);
            String time = message.getString(MESSAGE_TIME_KEY);
            List<Object> contents = JsonUtil.getListFromJsonArray(message.
                getJSONArray(MESSAGE_CONTENT_KEY), true);
            // Assumes that the server is going to return messages in
            // chronological order.
            if (requestedType.equals("")) {
              instance.putMessageTime(requestedType, time);
            }
            instance.putMessageTime(type, time);
            GotMessage(type, sender, contents);
          }
        } catch (JSONException e) {
          Log.w(LOG_TAG, e);
          Info("Failed to parse messages response.");
        }
        FunctionCompleted("GetMessages");
      }

      public void onFailure(String message) {
        WebServiceError("GetMessages", message);
      }
    };

    if (InstanceId().equals("")) {
      Info("You must join an instance before attempting to fetch messages.");
      return;
    }

    postCommandToGameServer(GET_MESSAGES_COMMAND,
        Lists.<NameValuePair>newArrayList(
            new BasicNameValuePair(GAME_ID_KEY, GameId()),
            new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
            new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
            new BasicNameValuePair(COUNT_KEY, Integer.toString(count)),
            new BasicNameValuePair(MESSAGE_TIME_KEY, instance.getMessageTime(requestedType)),
            new BasicNameValuePair(TYPE_KEY, requestedType)),
        myCallback);
  }

  /**
   * Invites a player to this game instance.
   *
   * Players implicitly accept invitations when they join games by
   * setting the instance id in their GameClient.
   *
   * Invitations remain active as long as the game instance exists.
   *
   * @param playerEmail a string containing the email address of the
   * player to become leader. The email should be in one of the
   * following formats:<br>"Name O. Person
   * &[email protected]&gt"<br>"[email protected]".
   */
  @SimpleFunction(
      description = "Invites a player to this game instance.")
  public void Invite(final String playerEmail) {
    AsynchUtil.runAsynchronously(new Runnable() {
      public void run() { postInvite(playerEmail); }});
  }

  private void postInvite(final String inviteeEmail) {
    AsyncCallbackPair<JSONObject> inviteCallback = new AsyncCallbackPair<JSONObject>(){
      public void onSuccess(final JSONObject response) {
        try {
          String invitedPlayer = response.getString(INVITEE_KEY);

          if (invitedPlayer.equals("")) {
            Info(invitedPlayer + " was already invited.");
          } else {
            Info("Successfully invited " + invitedPlayer + ".");
          }
        } catch (JSONException e) {
          Log.w(LOG_TAG, e);
          Info("Failed to parse invite player response.");
        }
        FunctionCompleted("Invite");
      }
      public void onFailure(final String message) {
        WebServiceError("Invite", message);
      }
    };

    if (InstanceId().equals("")) {
      Info("You must have joined an instance before you can invite new players.");
      return;
    }

    postCommandToGameServer(INVITE_COMMAND,
        Lists.<NameValuePair>newArrayList(
            new BasicNameValuePair(GAME_ID_KEY, GameId()),
            new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
            new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
            new BasicNameValuePair(INVITEE_KEY, inviteeEmail)),
        inviteCallback);
  }

  /**
   * Requests to leave the current instance. If the player is the
   * current leader, the lead will be passed to another player.
   *
   * If there are no other players left in the instance after the
   * current player leaves, the instance will become unjoinable.
   *
   * Upon successful completion of this command, the instance
   * lists will be updated and InstanceId will be set back to the
   * empty string.
   *
   * Note that while this call does clear the leader and player
   * lists, no NewLeader or PlayerLeft events are raised.
   */
  @SimpleFunction(description = "Leaves the current instance.")
  public void LeaveInstance() {
    AsynchUtil.runAsynchronously(new Runnable() {
      public void run() {
        postLeaveInstance();
      }
    });
  }

  private void postLeaveInstance() {
    AsyncCallbackPair<JSONObject> setInstanceCallback = new AsyncCallbackPair<JSONObject>(){
      public void onSuccess(final JSONObject response) {
        SetInstance("");
        processInstanceLists(response);
        FunctionCompleted("LeaveInstance");
      }
      public void onFailure(final String message) {
        WebServiceError("LeaveInstance", message);
      }
    };

    postCommandToGameServer(LEAVE_INSTANCE_COMMAND,
        Lists.<NameValuePair>newArrayList(
            new BasicNameValuePair(GAME_ID_KEY, GameId()),
            new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
            new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress())),
            setInstanceCallback);
  }

  /**
   * Creates a new game instance.  The instance has a unique
   * instanceId, and the leader is the player who created it. The
   * player that creates the game automatically joins it without
   * being sent an invitation.
   *
   * The actual instance id could differ from the instanceId
   * specified because the game server will enforce uniqueness. The
   * actual instanceId will be provided to AppInventor when a
   * NewInstanceMade event triggers upon successful completion of
   * this server request.
   *
   * @param instanceId A string to use as for the instance
   * id. If no other instance exists with this id, the new instance
   * will have this id. However, since the id must be unique, if
   * another instance exists with the same one, then a number
   * will be appended to the end of this prefix.
   * @param makePublic A boolean indicating whether or not the
   * instance should be publicly viewable and able to be joined by
   * anyone.
   */
  @SimpleFunction(description = "Asks the server to create a new " +
                "instance of this game.")
  public void MakeNewInstance(final String instanceId, final boolean makePublic) {
    AsynchUtil.runAsynchronously(new Runnable() {
      public void run() { postMakeNewInstance(instanceId, makePublic); }});
  }

  private void postMakeNewInstance(final String requestedInstanceId, final Boolean makePublic) {
    AsyncCallbackPair<JSONObject> makeNewGameCallback = new AsyncCallbackPair<JSONObject>(){
      public void onSuccess(final JSONObject response) {
        processInstanceLists(response);
        NewInstanceMade(InstanceId());
        FunctionCompleted("MakeNewInstance");
      }
      public void onFailure(final String message) {
        WebServiceError("MakeNewInstance", message);
      }
    };

    postCommandToGameServer(NEW_INSTANCE_COMMAND,
        Lists.<NameValuePair>newArrayList(
            new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
            new BasicNameValuePair(GAME_ID_KEY, GameId()),
            new BasicNameValuePair(INSTANCE_ID_KEY, requestedInstanceId),
            new BasicNameValuePair(INSTANCE_PUBLIC_KEY, makePublic.toString())),
            makeNewGameCallback, true);
  }

  /**
   * Creates a new message and sends it to the stated recipients.
   *
   * @param type A "key" for the message. This identifies the type of
   * message so that when other players receive the message they know
   * how to properly handle it.
   * @param recipients If set to an empty list, the server will send
   * this message with a blank set of recipients, meaning that all
   * players in the instance are able to retrieve it. To limit the
   * message receipt to a single person or a group of people,
   * recipients should be a list of the email addresses of the people
   * meant to receive the message. Each email should be in one of the
   * following formats:<br>
   * "Name O. Person &[email protected]&gt"<br>
   * "[email protected]"
   * @param contents the contents of the message. This can be any
   * AppInventor data value.
   */
  @SimpleFunction(description = "Sends a keyed message to all " +
                "recipients in the recipients list. The message will " +
                "consist of the contents list.")
  public void SendMessage(final String type, final YailList recipients, final YailList contents) {
    AsynchUtil.runAsynchronously(new Runnable() {
      public void run() { postNewMessage(type, recipients, contents); }});
  }

  private void postNewMessage(final String type, YailList recipients, YailList contents){
    AsyncCallbackPair<JSONObject> myCallback = new AsyncCallbackPair<JSONObject>(){
      public void onSuccess(final JSONObject response) {
        FunctionCompleted("SendMessage");
      }
      public void onFailure(final String message) {
        WebServiceError("SendMessage", message);
      }
    };

    if (InstanceId().equals("")) {
      Info("You must have joined an instance before you can send messages.");
      return;
    }

    postCommandToGameServer(NEW_MESSAGE_COMMAND,
        Lists.<NameValuePair>newArrayList(
            new BasicNameValuePair(GAME_ID_KEY, GameId()),
            new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
            new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
            new BasicNameValuePair(TYPE_KEY, type),
            new BasicNameValuePair(MESSAGE_RECIPIENTS_KEY, recipients.toJSONString()),
            new BasicNameValuePair(MESSAGE_CONTENT_KEY, contents.toJSONString()),
            new BasicNameValuePair(MESSAGE_TIME_KEY, instance.getMessageTime(type))),
        myCallback);
  }

  /**
   * Submits a command to the game server. Server commands are
   * custom actions that are performed on the server. The arguments
   * required and return value of a server command depend on its
   * implementation.
   *
   * For more information about server commands, consult the game
   * server code at:
   * http://code.google.com/p/app-inventor-for-android/
   *
   * @param command The name of the server command.
   * @param arguments The arguments to pass to the server to specify
   * how to execute the command.
   */
  @SimpleFunction(description = "Sends the specified command to " +
                "the game server.")
  public void ServerCommand(final String command, final YailList arguments) {
    AsynchUtil.runAsynchronously(new Runnable() {
      public void run() { postServerCommand(command, arguments); }});
  }

  private void postServerCommand(final String command, final YailList arguments){
    AsyncCallbackPair<JSONObject> myCallback = new AsyncCallbackPair<JSONObject>() {
      public void onSuccess(final JSONObject result) {
        try {
          ServerCommandSuccess(command, JsonUtil.getListFromJsonArray(result.
              getJSONArray(MESSAGE_CONTENT_KEY), true));
        } catch (JSONException e) {
          Log.w(LOG_TAG, e);
          Info("Server command response failed to parse.");
        }
        FunctionCompleted("ServerCommand");
      }

      public void onFailure(String message) {
        ServerCommandFailure(command, arguments);
        WebServiceError("ServerCommand", message);
      }
    };

    Log.d(LOG_TAG, "Going to post " + command + " with args " + arguments);
    postCommandToGameServer(SERVER_COMMAND,
        Lists.<NameValuePair>newArrayList(
            new BasicNameValuePair(GAME_ID_KEY, GameId()),
            new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
            new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
            new BasicNameValuePair(COMMAND_TYPE_KEY, command),
            new BasicNameValuePair(COMMAND_ARGUMENTS_KEY, arguments.toJSONString())),
        myCallback);
  }

  /**
   * Specifies the game instance id.  Taken together, the game ID and
   * the instance ID uniquely identify the game.
   *
   * @param instanceId the name of the game instance to join.
   */
  @SimpleFunction(description = "Sets InstanceId and joins the " +
                "specified instance.")
  public void SetInstance(final String instanceId) {
    AsynchUtil.runAsynchronously(new Runnable() {
      public void run() {
        if (instanceId.equals("")) {
          Log.d(LOG_TAG, "Instance id set to empty string.");
          if (!InstanceId().equals("")) {
            instance = new GameInstance("");
            InstanceIdChanged("");
            FunctionCompleted("SetInstance");
          }
        } else {
          postSetInstance(instanceId);
        }
      }
    });
  }

  private void postSetInstance(String instanceId) {
    AsyncCallbackPair<JSONObject> setInstanceCallback = new AsyncCallbackPair<JSONObject>(){
      public void onSuccess(final JSONObject response) {
        processInstanceLists(response);
        FunctionCompleted("SetInstance");
      }
      public void onFailure(final String message) {
        WebServiceError("SetInstance", message);
      }
    };

    postCommandToGameServer(JOIN_INSTANCE_COMMAND,
        Lists.<NameValuePair>newArrayList(
            new BasicNameValuePair(GAME_ID_KEY, GameId()),
            new BasicNameValuePair(INSTANCE_ID_KEY, instanceId),
            new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress())),
            setInstanceCallback, true);
  }

  /**
   * Specifies the game's leader. At any time, each game instance
   * has only one leader, but the leader may change over time.
   * Initially, the leader is the game instance creator. Application
   * inventors determine special properties of the leader.
   *
   * The leader can only be set by the current leader of the game.
   *
   * @param playerEmail a string containing the email address of the
   * player to become leader. The email should be in one of the
   * following formats:
   * <br>"Name O. Person &[email protected]&gt"
   * <br>"[email protected]".
   */
  @SimpleFunction(description = "Tells the server to set the " +
                "leader to playerId. Only the current leader may " +
                "successfully set a new leader.")
  public void SetLeader(final String playerEmail) {
    AsynchUtil.runAsynchronously(new Runnable() {
      public void run() { postSetLeader(playerEmail); }});
  }

  private void postSetLeader(final String newLeader) {
    AsyncCallbackPair<JSONObject> setLeaderCallback = new AsyncCallbackPair<JSONObject>(){
      public void onSuccess(final JSONObject response) {
        FunctionCompleted("SetLeader");
      }
      public void onFailure(final String message) {
        WebServiceError("SetLeader", message);
      }
    };

    if (InstanceId().equals("")) {
      Info("You must join an instance before attempting to set a leader.");
      return;
    }

    postCommandToGameServer(SET_LEADER_COMMAND,
        Lists.<NameValuePair>newArrayList(
            new BasicNameValuePair(GAME_ID_KEY, GameId()),
            new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
            new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
            new BasicNameValuePair(LEADER_KEY, newLeader)),
            setLeaderCallback);
  }

  //----------------------------------------------------------------
  // Activity Lifecycle Management

  /**
   * Called automatically by the operating system.
   *
   * Currently does nothing.
   */
  public void onResume() {
    Log.d(LOG_TAG, "Activity Resumed.");
  }

  /**
   * Called automatically by the operating system.
   *
   * Currently does nothing.
   */
  public void onStop() {
    Log.d(LOG_TAG, "Activity Stopped.");
  }

  //----------------------------------------------------------------
  // Utility Methods

  private void postCommandToGameServer(final String commandName,
      List<NameValuePair> params, final AsyncCallbackPair<JSONObject> callback) {
    postCommandToGameServer(commandName, params, callback, false);
  }

  private void postCommandToGameServer(final String commandName,
      final List<NameValuePair> params, final AsyncCallbackPair<JSONObject> callback,
      final boolean allowInstanceIdChange) {
    AsyncCallbackPair<JSONObject> thisCallback = new AsyncCallbackPair<JSONObject>() {
      public void onSuccess(JSONObject responseObject) {
        Log.d(LOG_TAG, "Received response for " + commandName + ": " + responseObject.toString());

        try {
          if (responseObject.getBoolean(ERROR_RESPONSE_KEY)) {
            callback.onFailure(responseObject.getString(SERVER_RETURN_VALUE_KEY));
          } else {
            String responseGameId = responseObject.getString(GAME_ID_KEY);
            if (!responseGameId.equals(GameId())) {
              Info("Incorrect game id in response: + " + responseGameId + ".");
              return;
            }
            String responseInstanceId = responseObject.getString(INSTANCE_ID_KEY);
            if (responseInstanceId.equals("")) {
              callback.onSuccess(responseObject.getJSONObject(SERVER_RETURN_VALUE_KEY));
              return;
            }

            if (responseInstanceId.equals(InstanceId())) {
              updateInstanceInfo(responseObject);
            } else {
              if (allowInstanceIdChange || InstanceId().equals("")) {
                instance = new GameInstance(responseInstanceId);
                updateInstanceInfo(responseObject);
                InstanceIdChanged(responseInstanceId);
              } else {
                Info("Ignored server response to " + commandName + " for incorrect instance " +
                    responseInstanceId + ".");
                return;
              }
            }
            callback.onSuccess(responseObject.getJSONObject(SERVER_RETURN_VALUE_KEY));
          }
        } catch (JSONException e) {
          Log.w(LOG_TAG, e);
          callback.onFailure("Failed to parse JSON response to command " + commandName);
        }
      }
      public void onFailure(String failureMessage) {
        Log.d(LOG_TAG, "Posting to server failed for " + commandName + " with arguments " +
            params + "\n Failure message: " +  failureMessage);
        callback.onFailure(failureMessage);
      }
    };

    WebServiceUtil.getInstance().postCommandReturningObject(ServiceUrl(), commandName, params,
        thisCallback);
  }

  private void updateInstanceInfo(JSONObject responseObject) throws JSONException {
    boolean newLeader = false;
    String leader = responseObject.getString(LEADER_KEY);
    List<String> receivedPlayers = JsonUtil.getStringListFromJsonArray(responseObject.
        getJSONArray(PLAYERS_LIST_KEY));

    if (!Leader().equals(leader)) {
      instance.setLeader(leader);
      newLeader = true;
    }

    PlayerListDelta playersDelta = instance.setPlayers(receivedPlayers);
    if (playersDelta != PlayerListDelta.NO_CHANGE) {
      for (final String player : playersDelta.getPlayersRemoved()) {
        PlayerLeft(player);
      }
      for (final String player : playersDelta.getPlayersAdded()) {
        PlayerJoined(player);
      }
    }

    if (newLeader) {
      NewLeader(Leader());
    }
  }
}