/*
 * Copyright (C) 2013 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.example.games.tbmpskeleton;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.games.Games;
import com.google.android.gms.games.GamesCallbackStatusCodes;
import com.google.android.gms.games.GamesClient;
import com.google.android.gms.games.GamesClientStatusCodes;
import com.google.android.gms.games.InvitationsClient;
import com.google.android.gms.games.Player;
import com.google.android.gms.games.TurnBasedMultiplayerClient;
import com.google.android.gms.games.multiplayer.Invitation;
import com.google.android.gms.games.multiplayer.InvitationCallback;
import com.google.android.gms.games.multiplayer.Multiplayer;
import com.google.android.gms.games.multiplayer.realtime.RoomConfig;
import com.google.android.gms.games.multiplayer.turnbased.TurnBasedMatch;
import com.google.android.gms.games.multiplayer.turnbased.TurnBasedMatchConfig;
import com.google.android.gms.games.multiplayer.turnbased.TurnBasedMatchUpdateCallback;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;

import java.util.ArrayList;


/**
 * TBMPSkeleton: A minimalistic "game" that shows turn-based
 * multiplayer features for Play Games Services.  In this game, you
 * can invite a variable number of players and take turns editing a
 * shared state, which consists of single string.  You can also select
 * automatch players; all known players play before automatch slots
 * are filled.
 * <p>
 * INSTRUCTIONS: To run this sample, please set up
 * a project in the Developer Console. Then, place your app ID on
 * res/values/ids.xml. Also, change the package name to the package name you
 * used to create the client ID in Developer Console. Make sure you sign the
 * APK with the certificate whose fingerprint you entered in Developer Console
 * when creating your Client Id.
 *
 * @author Wolff ([email protected]), 2013
 */
public class SkeletonActivity extends Activity implements
    View.OnClickListener {

  public static final String TAG = "SkeletonActivity";

  // Client used to sign in with Google APIs
  private GoogleSignInClient mGoogleSignInClient = null;

  // Client used to interact with the TurnBasedMultiplayer system.
  private TurnBasedMultiplayerClient mTurnBasedMultiplayerClient = null;

  // Client used to interact with the Invitation system.
  private InvitationsClient mInvitationsClient = null;

  // Local convenience pointers
  public TextView mDataView;
  public TextView mTurnTextView;

  private AlertDialog mAlertDialog;

  // For our intents
  private static final int RC_SIGN_IN = 9001;
  final static int RC_SELECT_PLAYERS = 10000;
  final static int RC_LOOK_AT_MATCHES = 10001;

  // Should I be showing the turn API?
  public boolean isDoingTurn = false;

  // This is the current match we're in; null if not loaded
  public TurnBasedMatch mMatch;

  // This is the current match data after being unpersisted.
  // Do not retain references to match data once you have
  // taken an action on the match, such as takeTurn()
  public SkeletonTurn mTurnData;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Create the Google API Client with access to Games
    // Create the client used to sign in.
    mGoogleSignInClient = GoogleSignIn.getClient(this, GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN);

    // Setup signin and signout buttons
    findViewById(R.id.sign_out_button).setOnClickListener(this);
    findViewById(R.id.sign_in_button).setOnClickListener(this);

    mDataView = findViewById(R.id.data_view);
    mTurnTextView = findViewById(R.id.turn_counter_view);
    checkPlaceholderIds();
  }

  // Check the sample to ensure all placeholder ids are are updated with real-world values.
  // This is strictly for the purpose of the samples; you don't need this in a production
  // application.
  private void checkPlaceholderIds() {
    StringBuilder problems = new StringBuilder();

    if (getPackageName().startsWith("com.google.")) {
      problems.append("- Package name start with com.google.*\n");
    }

    for (Integer id : new Integer[]{R.string.app_id}) {

      String value = getString(id);

      if (value.startsWith("YOUR_")) {
        // needs replacing
        problems.append("- Placeholders(YOUR_*) in ids.xml need updating\n");
        break;
      }
    }

    if (problems.length() > 0) {
      problems.insert(0, "The following problems were found:\n\n");

      problems.append("\nThese problems may prevent the app from working properly.");
      problems.append("\n\nSee the TODO window in Android Studio for more information");
      (new AlertDialog.Builder(this)).setMessage(problems.toString())
          .setNeutralButton(android.R.string.ok, null).create().show();
    }
  }

  @Override
  protected void onResume() {
    super.onResume();
    Log.d(TAG, "onResume()");

    // Since the state of the signed in user can change when the activity is not active
    // it is recommended to try and sign in silently from when the app resumes.
    signInSilently();
  }

  @Override
  protected void onPause() {
    super.onPause();

    // Unregister the invitation callbacks; they will be re-registered via
    // onResume->signInSilently->onConnected.
    if (mInvitationsClient != null) {
      mInvitationsClient.unregisterInvitationCallback(mInvitationCallback);
    }

    if (mTurnBasedMultiplayerClient != null) {
      mTurnBasedMultiplayerClient.unregisterTurnBasedMatchUpdateCallback(mMatchUpdateCallback);
    }
  }

  private String mDisplayName;
  private String mPlayerId;

  private void onConnected(GoogleSignInAccount googleSignInAccount) {
    Log.d(TAG, "onConnected(): connected to Google APIs");

    mTurnBasedMultiplayerClient = Games.getTurnBasedMultiplayerClient(this, googleSignInAccount);
    mInvitationsClient = Games.getInvitationsClient(this, googleSignInAccount);

    Games.getPlayersClient(this, googleSignInAccount)
        .getCurrentPlayer()
        .addOnSuccessListener(
            new OnSuccessListener<Player>() {
              @Override
              public void onSuccess(Player player) {
                mDisplayName = player.getDisplayName();
                mPlayerId = player.getPlayerId();

                setViewVisibility();
              }
            }
        )
        .addOnFailureListener(createFailureListener("There was a problem getting the player!"));

    Log.d(TAG, "onConnected(): Connection successful");

    // Retrieve the TurnBasedMatch from the connectionHint
    GamesClient gamesClient = Games.getGamesClient(this, googleSignInAccount);
    gamesClient.getActivationHint()
        .addOnSuccessListener(new OnSuccessListener<Bundle>() {
          @Override
          public void onSuccess(Bundle hint) {
            if (hint != null) {
              TurnBasedMatch match = hint.getParcelable(Multiplayer.EXTRA_TURN_BASED_MATCH);

              if (match != null) {
                updateMatch(match);
              }
            }
          }
        })
        .addOnFailureListener(createFailureListener(
            "There was a problem getting the activation hint!"));

    setViewVisibility();

    // As a demonstration, we are registering this activity as a handler for
    // invitation and match events.

    // This is *NOT* required; if you do not register a handler for
    // invitation events, you will get standard notifications instead.
    // Standard notifications may be preferable behavior in many cases.
    mInvitationsClient.registerInvitationCallback(mInvitationCallback);

    // Likewise, we are registering the optional MatchUpdateListener, which
    // will replace notifications you would get otherwise. You do *NOT* have
    // to register a MatchUpdateListener.
    mTurnBasedMultiplayerClient.registerTurnBasedMatchUpdateCallback(mMatchUpdateCallback);
  }

  private void onDisconnected() {

    Log.d(TAG, "onDisconnected()");

    mTurnBasedMultiplayerClient = null;
    mInvitationsClient = null;

    setViewVisibility();
  }

  // This is a helper functio that will do all the setup to create a simple failure message.
  // Add it to any task and in the case of an failure, it will report the string in an alert
  // dialog.
  private OnFailureListener createFailureListener(final String string) {
    return new OnFailureListener() {
      @Override
      public void onFailure(@NonNull Exception e) {
        handleException(e, string);
      }
    };
  }

  // Displays your inbox. You will get back onActivityResult where
  // you will need to figure out what you clicked on.
  public void onCheckGamesClicked(View view) {
    mTurnBasedMultiplayerClient.getInboxIntent()
        .addOnSuccessListener(new OnSuccessListener<Intent>() {
          @Override
          public void onSuccess(Intent intent) {
            startActivityForResult(intent, RC_LOOK_AT_MATCHES);
          }
        })
        .addOnFailureListener(createFailureListener(getString(R.string.error_get_inbox_intent)));
  }

  // Open the create-game UI. You will get back an onActivityResult
  // and figure out what to do.
  public void onStartMatchClicked(View view) {
    mTurnBasedMultiplayerClient.getSelectOpponentsIntent(1, 7, true)
        .addOnSuccessListener(new OnSuccessListener<Intent>() {
          @Override
          public void onSuccess(Intent intent) {
            startActivityForResult(intent, RC_SELECT_PLAYERS);
          }
        })
        .addOnFailureListener(createFailureListener(
            getString(R.string.error_get_select_opponents)));
  }

  // Create a one-on-one automatch game.
  public void onQuickMatchClicked(View view) {

    Bundle autoMatchCriteria = RoomConfig.createAutoMatchCriteria(1, 1, 0);

    TurnBasedMatchConfig turnBasedMatchConfig = TurnBasedMatchConfig.builder()
        .setAutoMatchCriteria(autoMatchCriteria).build();

    showSpinner();

    // Start the match
    mTurnBasedMultiplayerClient.createMatch(turnBasedMatchConfig)
        .addOnSuccessListener(new OnSuccessListener<TurnBasedMatch>() {
          @Override
          public void onSuccess(TurnBasedMatch turnBasedMatch) {
            onInitiateMatch(turnBasedMatch);
          }
        })
        .addOnFailureListener(createFailureListener("There was a problem creating a match!"));
  }

  // In-game controls

  // Cancel the game. Should possibly wait until the game is canceled before
  // giving up on the view.
  public void onCancelClicked(View view) {
    showSpinner();

    mTurnBasedMultiplayerClient.cancelMatch(mMatch.getMatchId())
        .addOnSuccessListener(new OnSuccessListener<String>() {
          @Override
          public void onSuccess(String matchId) {
            onCancelMatch(matchId);
          }
        })
        .addOnFailureListener(createFailureListener("There was a problem cancelling the match!"));

    isDoingTurn = false;
    setViewVisibility();
  }

  // Leave the game during your turn. Note that there is a separate
  // mTurnBasedMultiplayerClient.leaveMatch() if you want to leave NOT on your turn.
  public void onLeaveClicked(View view) {
    showSpinner();
    String nextParticipantId = getNextParticipantId();

    mTurnBasedMultiplayerClient.leaveMatchDuringTurn(mMatch.getMatchId(), nextParticipantId)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
          @Override
          public void onSuccess(Void aVoid) {
            onLeaveMatch();
          }
        })
        .addOnFailureListener(createFailureListener("There was a problem leaving the match!"));

    setViewVisibility();
  }

  // Finish the game. Sometimes, this is your only choice.
  public void onFinishClicked(View view) {
    showSpinner();
    mTurnBasedMultiplayerClient.finishMatch(mMatch.getMatchId())
        .addOnSuccessListener(new OnSuccessListener<TurnBasedMatch>() {
          @Override
          public void onSuccess(TurnBasedMatch turnBasedMatch) {
            onUpdateMatch(turnBasedMatch);
          }
        })
        .addOnFailureListener(createFailureListener("There was a problem finishing the match!"));

    isDoingTurn = false;
    setViewVisibility();
  }


  // Upload your new gamestate, then take a turn, and pass it on to the next
  // player.
  public void onDoneClicked(View view) {
    showSpinner();

    String nextParticipantId = getNextParticipantId();
    // Create the next turn
    mTurnData.turnCounter += 1;
    mTurnData.data = mDataView.getText().toString();

    mTurnBasedMultiplayerClient.takeTurn(mMatch.getMatchId(),
        mTurnData.persist(), nextParticipantId)
        .addOnSuccessListener(new OnSuccessListener<TurnBasedMatch>() {
          @Override
          public void onSuccess(TurnBasedMatch turnBasedMatch) {
            onUpdateMatch(turnBasedMatch);
          }
        })
        .addOnFailureListener(createFailureListener("There was a problem taking a turn!"));

    mTurnData = null;
  }

  // Sign-in, Sign out behavior

  // Update the visibility based on what state we're in.
  public void setViewVisibility() {
    boolean isSignedIn = mTurnBasedMultiplayerClient != null;

    if (!isSignedIn) {
      findViewById(R.id.login_layout).setVisibility(View.VISIBLE);
      findViewById(R.id.sign_in_button).setVisibility(View.VISIBLE);
      findViewById(R.id.matchup_layout).setVisibility(View.GONE);
      findViewById(R.id.gameplay_layout).setVisibility(View.GONE);

      if (mAlertDialog != null) {
        mAlertDialog.dismiss();
      }
      return;
    }


    ((TextView) findViewById(R.id.name_field)).setText(mDisplayName);
    findViewById(R.id.login_layout).setVisibility(View.GONE);

    if (isDoingTurn) {
      findViewById(R.id.matchup_layout).setVisibility(View.GONE);
      findViewById(R.id.gameplay_layout).setVisibility(View.VISIBLE);
    } else {
      findViewById(R.id.matchup_layout).setVisibility(View.VISIBLE);
      findViewById(R.id.gameplay_layout).setVisibility(View.GONE);
    }
  }

  // Switch to gameplay view.
  public void setGameplayUI() {
    isDoingTurn = true;
    setViewVisibility();
    mDataView.setText(mTurnData.data);
    mTurnTextView.setText(getString(R.string.turn_label, mTurnData.turnCounter));
  }

  // Helpful dialogs

  public void showSpinner() {
    findViewById(R.id.progressLayout).setVisibility(View.VISIBLE);
  }

  public void dismissSpinner() {
    findViewById(R.id.progressLayout).setVisibility(View.GONE);
  }

  // Generic warning/info dialog
  public void showWarning(String title, String message) {
    AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);

    // set title
    alertDialogBuilder.setTitle(title).setMessage(message);

    // set dialog message
    alertDialogBuilder.setCancelable(false).setPositiveButton("OK",
        new DialogInterface.OnClickListener() {
          @Override
          public void onClick(DialogInterface dialog, int id) {
            // if this button is clicked, close
            // current activity
          }
        });

    // create alert dialog
    mAlertDialog = alertDialogBuilder.create();

    // show it
    mAlertDialog.show();
  }

  // Rematch dialog
  public void askForRematch() {
    AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);

    alertDialogBuilder.setMessage("Do you want a rematch?");

    alertDialogBuilder
        .setCancelable(false)
        .setPositiveButton("Sure, rematch!",
            new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int id) {
                rematch();
              }
            })
        .setNegativeButton("No.",
            new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int id) {
              }
            });

    alertDialogBuilder.show();
  }

  /**
   * Start a sign in activity.  To properly handle the result, call tryHandleSignInResult from
   * your Activity's onActivityResult function
   */
  public void startSignInIntent() {
    startActivityForResult(mGoogleSignInClient.getSignInIntent(), RC_SIGN_IN);
  }

  /**
   * Try to sign in without displaying dialogs to the user.
   * <p>
   * If the user has already signed in previously, it will not show dialog.
   */
  public void signInSilently() {
    Log.d(TAG, "signInSilently()");

    mGoogleSignInClient.silentSignIn().addOnCompleteListener(this,
        new OnCompleteListener<GoogleSignInAccount>() {
          @Override
          public void onComplete(@NonNull Task<GoogleSignInAccount> task) {
            if (task.isSuccessful()) {
              Log.d(TAG, "signInSilently(): success");
              onConnected(task.getResult());
            } else {
              Log.d(TAG, "signInSilently(): failure", task.getException());
              onDisconnected();
            }
          }
        });
  }

  public void signOut() {
    Log.d(TAG, "signOut()");

    mGoogleSignInClient.signOut().addOnCompleteListener(this,
        new OnCompleteListener<Void>() {
          @Override
          public void onComplete(@NonNull Task<Void> task) {

            if (task.isSuccessful()) {
              Log.d(TAG, "signOut(): success");
            } else {
              handleException(task.getException(), "signOut() failed!");
            }

            onDisconnected();
          }
        });
  }

  /**
   * Since a lot of the operations use tasks, we can use a common handler for whenever one fails.
   *
   * @param exception The exception to evaluate.  Will try to display a more descriptive reason for
   *                  the exception.
   * @param details   Will display alongside the exception if you wish to provide more details for
   *                  why the exception happened
   */
  private void handleException(Exception exception, String details) {
    int status = 0;

    if (exception instanceof TurnBasedMultiplayerClient.MatchOutOfDateApiException) {
      TurnBasedMultiplayerClient.MatchOutOfDateApiException matchOutOfDateApiException =
          (TurnBasedMultiplayerClient.MatchOutOfDateApiException) exception;

      new AlertDialog.Builder(this)
          .setMessage("Match was out of date, updating with latest match data...")
          .setNeutralButton(android.R.string.ok, null)
          .show();

      TurnBasedMatch match = matchOutOfDateApiException.getMatch();
      updateMatch(match);

      return;
    }

    if (exception instanceof ApiException) {
      ApiException apiException = (ApiException) exception;
      status = apiException.getStatusCode();
    }

    if (!checkStatusCode(status)) {
      return;
    }

    String message = getString(R.string.status_exception_error, details, status, exception);

    new AlertDialog.Builder(this)
        .setMessage(message)
        .setNeutralButton(android.R.string.ok, null)
        .show();
  }

  private void logBadActivityResult(int requestCode, int resultCode, String message) {
    Log.i(TAG, "Bad activity result(" + resultCode + ") for request (" + requestCode + "): "
        + message);
  }

  // This function is what gets called when you return from either the Play
  // Games built-in inbox, or else the create game built-in interface.
  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent intent) {

    if (requestCode == RC_SIGN_IN) {

      Task<GoogleSignInAccount> task =
          GoogleSignIn.getSignedInAccountFromIntent(intent);

      try {
        GoogleSignInAccount account = task.getResult(ApiException.class);
        onConnected(account);
      } catch (ApiException apiException) {
        String message = apiException.getMessage();
        if (message == null || message.isEmpty()) {
          message = getString(R.string.signin_other_error);
        }

        onDisconnected();

        new AlertDialog.Builder(this)
            .setMessage(message)
            .setNeutralButton(android.R.string.ok, null)
            .show();
      }
    } else if (requestCode == RC_LOOK_AT_MATCHES) {
      // Returning from the 'Select Match' dialog

      if (resultCode != Activity.RESULT_OK) {
        logBadActivityResult(requestCode, resultCode,
            "User cancelled returning from the 'Select Match' dialog.");
        return;
      }

      TurnBasedMatch match = intent
          .getParcelableExtra(Multiplayer.EXTRA_TURN_BASED_MATCH);

      if (match != null) {
        updateMatch(match);
      }

      Log.d(TAG, "Match = " + match);
    } else if (requestCode == RC_SELECT_PLAYERS) {
      // Returning from 'Select players to Invite' dialog

      if (resultCode != Activity.RESULT_OK) {
        // user canceled
        logBadActivityResult(requestCode, resultCode,
            "User cancelled returning from 'Select players to Invite' dialog");
        return;
      }

      // get the invitee list
      ArrayList<String> invitees = intent
          .getStringArrayListExtra(Games.EXTRA_PLAYER_IDS);

      // get automatch criteria
      Bundle autoMatchCriteria;

      int minAutoMatchPlayers = intent.getIntExtra(Multiplayer.EXTRA_MIN_AUTOMATCH_PLAYERS, 0);
      int maxAutoMatchPlayers = intent.getIntExtra(Multiplayer.EXTRA_MAX_AUTOMATCH_PLAYERS, 0);

      if (minAutoMatchPlayers > 0) {
        autoMatchCriteria = RoomConfig.createAutoMatchCriteria(minAutoMatchPlayers,
            maxAutoMatchPlayers, 0);
      } else {
        autoMatchCriteria = null;
      }

      TurnBasedMatchConfig tbmc = TurnBasedMatchConfig.builder()
          .addInvitedPlayers(invitees)
          .setAutoMatchCriteria(autoMatchCriteria).build();

      // Start the match
      mTurnBasedMultiplayerClient.createMatch(tbmc)
          .addOnSuccessListener(new OnSuccessListener<TurnBasedMatch>() {
            @Override
            public void onSuccess(TurnBasedMatch turnBasedMatch) {
              onInitiateMatch(turnBasedMatch);
            }
          })
          .addOnFailureListener(createFailureListener("There was a problem creating a match!"));
      showSpinner();
    }
  }

  // startMatch() happens in response to the createTurnBasedMatch()
  // above. This is only called on success, so we should have a
  // valid match object. We're taking this opportunity to setup the
  // game, saving our initial state. Calling takeTurn() will
  // callback to OnTurnBasedMatchUpdated(), which will show the game
  // UI.
  public void startMatch(TurnBasedMatch match) {
    mTurnData = new SkeletonTurn();
    // Some basic turn data
    mTurnData.data = "First turn";

    mMatch = match;

    String myParticipantId = mMatch.getParticipantId(mPlayerId);

    showSpinner();

    mTurnBasedMultiplayerClient.takeTurn(match.getMatchId(),
        mTurnData.persist(), myParticipantId)
        .addOnSuccessListener(new OnSuccessListener<TurnBasedMatch>() {
          @Override
          public void onSuccess(TurnBasedMatch turnBasedMatch) {
            updateMatch(turnBasedMatch);
          }
        })
        .addOnFailureListener(createFailureListener("There was a problem taking a turn!"));
  }

  // If you choose to rematch, then call it and wait for a response.
  public void rematch() {
    showSpinner();
    mTurnBasedMultiplayerClient.rematch(mMatch.getMatchId())
        .addOnSuccessListener(new OnSuccessListener<TurnBasedMatch>() {
          @Override
          public void onSuccess(TurnBasedMatch turnBasedMatch) {
            onInitiateMatch(turnBasedMatch);
          }
        })
        .addOnFailureListener(createFailureListener("There was a problem starting a rematch!"));
    mMatch = null;
    isDoingTurn = false;
  }

  /**
   * Get the next participant. In this function, we assume that we are
   * round-robin, with all known players going before all automatch players.
   * This is not a requirement; players can go in any order. However, you can
   * take turns in any order.
   *
   * @return participantId of next player, or null if automatching
   */
  public String getNextParticipantId() {

    String myParticipantId = mMatch.getParticipantId(mPlayerId);

    ArrayList<String> participantIds = mMatch.getParticipantIds();

    int desiredIndex = -1;

    for (int i = 0; i < participantIds.size(); i++) {
      if (participantIds.get(i).equals(myParticipantId)) {
        desiredIndex = i + 1;
      }
    }

    if (desiredIndex < participantIds.size()) {
      return participantIds.get(desiredIndex);
    }

    if (mMatch.getAvailableAutoMatchSlots() <= 0) {
      // You've run out of automatch slots, so we start over.
      return participantIds.get(0);
    } else {
      // You have not yet fully automatched, so null will find a new
      // person to play against.
      return null;
    }
  }

  // This is the main function that gets called when players choose a match
  // from the inbox, or else create a match and want to start it.
  public void updateMatch(TurnBasedMatch match) {
    mMatch = match;

    int status = match.getStatus();
    int turnStatus = match.getTurnStatus();

    switch (status) {
      case TurnBasedMatch.MATCH_STATUS_CANCELED:
        showWarning("Canceled!", "This game was canceled!");
        return;
      case TurnBasedMatch.MATCH_STATUS_EXPIRED:
        showWarning("Expired!", "This game is expired.  So sad!");
        return;
      case TurnBasedMatch.MATCH_STATUS_AUTO_MATCHING:
        showWarning("Waiting for auto-match...",
            "We're still waiting for an automatch partner.");
        return;
      case TurnBasedMatch.MATCH_STATUS_COMPLETE:
        if (turnStatus == TurnBasedMatch.MATCH_TURN_STATUS_COMPLETE) {
          showWarning("Complete!",
              "This game is over; someone finished it, and so did you!  " +
                  "There is nothing to be done.");
          break;
        }

        // Note that in this state, you must still call "Finish" yourself,
        // so we allow this to continue.
        showWarning("Complete!",
            "This game is over; someone finished it!  You can only finish it now.");
    }

    // OK, it's active. Check on turn status.
    switch (turnStatus) {
      case TurnBasedMatch.MATCH_TURN_STATUS_MY_TURN:
        mTurnData = SkeletonTurn.unpersist(mMatch.getData());
        setGameplayUI();
        return;
      case TurnBasedMatch.MATCH_TURN_STATUS_THEIR_TURN:
        // Should return results.
        showWarning("Alas...", "It's not your turn.");
        break;
      case TurnBasedMatch.MATCH_TURN_STATUS_INVITED:
        showWarning("Good inititative!",
            "Still waiting for invitations.\n\nBe patient!");
    }

    mTurnData = null;

    setViewVisibility();
  }

  private void onCancelMatch(String matchId) {
    dismissSpinner();

    isDoingTurn = false;

    showWarning("Match", "This match (" + matchId + ") was canceled.  " +
        "All other players will have their game ended.");
  }

  private void onInitiateMatch(TurnBasedMatch match) {
    dismissSpinner();

    if (match.getData() != null) {
      // This is a game that has already started, so I'll just start
      updateMatch(match);
      return;
    }

    startMatch(match);
  }

  private void onLeaveMatch() {
    dismissSpinner();

    isDoingTurn = false;
    showWarning("Left", "You've left this match.");
  }


  public void onUpdateMatch(TurnBasedMatch match) {
    dismissSpinner();

    if (match.canRematch()) {
      askForRematch();
    }

    isDoingTurn = (match.getTurnStatus() == TurnBasedMatch.MATCH_TURN_STATUS_MY_TURN);

    if (isDoingTurn) {
      updateMatch(match);
      return;
    }

    setViewVisibility();
  }

  private InvitationCallback mInvitationCallback = new InvitationCallback() {
    // Handle notification events.
    @Override
    public void onInvitationReceived(@NonNull Invitation invitation) {
      Toast.makeText(
          SkeletonActivity.this,
          "An invitation has arrived from "
              + invitation.getInviter().getDisplayName(), Toast.LENGTH_SHORT)
          .show();
    }

    @Override
    public void onInvitationRemoved(@NonNull String invitationId) {
      Toast.makeText(SkeletonActivity.this, "An invitation was removed.", Toast.LENGTH_SHORT)
          .show();
    }
  };

  private TurnBasedMatchUpdateCallback mMatchUpdateCallback = new TurnBasedMatchUpdateCallback() {
    @Override
    public void onTurnBasedMatchReceived(@NonNull TurnBasedMatch turnBasedMatch) {
      Toast.makeText(SkeletonActivity.this, "A match was updated.", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onTurnBasedMatchRemoved(@NonNull String matchId) {
      Toast.makeText(SkeletonActivity.this, "A match was removed.", Toast.LENGTH_SHORT).show();
    }
  };

  public void showErrorMessage(int stringId) {
    showWarning("Warning", getResources().getString(stringId));
  }

  // Returns false if something went wrong, probably. This should handle
  // more cases, and probably report more accurate results.
  private boolean checkStatusCode(int statusCode) {
    switch (statusCode) {
      case GamesCallbackStatusCodes.OK:
        return true;

      case GamesClientStatusCodes.MULTIPLAYER_ERROR_NOT_TRUSTED_TESTER:
        showErrorMessage(R.string.status_multiplayer_error_not_trusted_tester);
        break;
      case GamesClientStatusCodes.MATCH_ERROR_ALREADY_REMATCHED:
        showErrorMessage(R.string.match_error_already_rematched);
        break;
      case GamesClientStatusCodes.NETWORK_ERROR_OPERATION_FAILED:
        showErrorMessage(R.string.network_error_operation_failed);
        break;
      case GamesClientStatusCodes.INTERNAL_ERROR:
        showErrorMessage(R.string.internal_error);
        break;
      case GamesClientStatusCodes.MATCH_ERROR_INACTIVE_MATCH:
        showErrorMessage(R.string.match_error_inactive_match);
        break;
      case GamesClientStatusCodes.MATCH_ERROR_LOCALLY_MODIFIED:
        showErrorMessage(R.string.match_error_locally_modified);
        break;
      default:
        showErrorMessage(R.string.unexpected_status);
        Log.d(TAG, "Did not have warning or string to deal with: "
            + statusCode);
    }

    return false;
  }

  @Override
  public void onClick(View v) {
    switch (v.getId()) {
      case R.id.sign_in_button:
        mMatch = null;
        findViewById(R.id.sign_in_button).setVisibility(View.GONE);
        startSignInIntent();
        break;
      case R.id.sign_out_button:
        signOut();
        setViewVisibility();
        break;
    }
  }
}