﻿#region File Description
//-----------------------------------------------------------------------------
// TicTacToeService.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion

#region Using Statements


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Threading;
using System.ComponentModel.Composition;
using WindowsPhone.Recipes.Push.Messasges;
using System.Net;
using System.Xml.Serialization;
using System.IO;
using System.Xml;


#endregion

namespace TicTacToeServices
{
    /// <summary>
    /// Handle the logic of the Tic Tac Toe game
    /// </summary>
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class TicTacToeService : ITicTacToeService
    {
        #region Fields

        // Const int
        private const int MinimumStepsToCheckVictory = 5;
        private const int RowMaxSize = 3;
        private const int ColumnMaxSize = 3;
        private const int TotalCellCount = RowMaxSize * ColumnMaxSize;

        private readonly Dictionary<Guid, Subscription> subscribers = new Dictionary<Guid, Subscription>();
        private GameState gameState = new GameState();


        #endregion

        #region Public Methods


        /// <summary>
        /// Register a user and adds the user to the notification list.
        /// </summary>
        /// <param name="SessionID">Session identifier generated by the client.</param>
        /// <param name="clientURI">The <see cref="Uri"/> supplied by the client. This Uri is used to send push 
        /// notifications to the client.
        public bool Register(Guid SessionID, Uri clientURI)
        {
            if (subscribers.Count > 1)
            {
                subscribers.Clear();
            }

            Subscription subscription = new Subscription(SessionID, clientURI);

            subscribers[SessionID] = subscription;

            gameState = new GameState();

            return true;
        }

        /// <summary>
        /// Unregister a user and removes the user from the notification list.
        /// </summary>
        /// <param name="SessionID">The game user's session identifier.</param>
        public void Unregister(Guid SessionID)
        {
            if (subscribers.Remove(SessionID))
            {
                // If there are no registered clients, release the game state
                if (subscribers.Count == 0)
                {
                    gameState = null;
                }
            }
        }

        /// <summary>
        /// Performs a step in the game and notifies all associated clients using a push notification.
        /// </summary>
        /// <param name="SessionID">Identifier of the client who performed the move.</param>
        /// <param name="x">The X position of the move performed.</param>
        /// <param name="y">The Y position of the move performed.</param>
        /// <param name="playerLetter">Letter representing the player's move. Either "X" or "O".</param>
        public void GameStep(Guid SessionID, int x, int y, string player)
        {
            TicTacToeMove move = new TicTacToeMove() { X = x, Y = y, Player = player };

            if (!ValidateTicTacToeMove(move))
            {
                return;
            }

            gameState.StepsMade++;

            // Perform the requested move
            MakeMove(move);

            // Update the game state in light of the move performed
            UpdateState(move);

            byte[] bytes = GetBytes<TicTacToeMove>(move);

            // Notify all the relevant clients of the move
            foreach (KeyValuePair<Guid, Subscription> sub in subscribers)
            {
                if (sub.Value.SessionID != SessionID)
                {
                    SendMessage(sub.Value.ChannelUri, bytes);
                }
            }
        }

        /// <summary>
        /// Sends the entire game state to all associated clients using push notifications.
        /// </summary>
        /// <param name="SessionID">Identifier of a user participating in a game the state of which is to 
        /// be sent.</param>
        public void GetGameState(Guid SessionID)
        {
            // Serialize the game state
            byte[] bytes = GetBytes<GameState>(gameState);

            SendMessage(subscribers[SessionID].ChannelUri, bytes);
        }

        /// <summary>
        /// Restart a specific game.
        /// </summary>
        /// <param name="SessionID">Identifier of a user participating in the game to restart.</param>
        public void RestartGame(Guid SessionID)
        {
            gameState = new GameState();
        }

        #endregion

        #region Private Methods


        /// <summary>
        /// Update the state of the game in light of a move performed by one of the clients. This checks whether or
        /// not the game has ended. If the game ended determines the game result and otherwise changes the currently
        /// active player.
        /// </summary>
        /// <param name="move">The move performed by one of the clients.</param>
        private void UpdateState(TicTacToeMove move)
        {
            bool isGameOver = false;

            // In order to win this game, a player must perform at least 3 steps.
            // Therefore, if we count 5 steps for both players it means that player "X" already made 3 steps.
            if (gameState.StepsMade >= MinimumStepsToCheckVictory)
            {
                int col = 0;
                int row = 0;
                int leftDiagonal = 0;
                int rightDiagonal = 0;

                // See if the player won by performing the supplied move
                for (int i = 0; i < RowMaxSize; i++)
                {
                    // Checks the move's column
                    if (gameState.Board[move.X][i] == move.Player)
                        col++;
                    // Checks the move's row
                    if (gameState.Board[i][move.Y] == move.Player)
                        row++;
                    // Checks the primary diagonal
                    if (gameState.Board[i][i] == move.Player)
                        leftDiagonal++;
                    // Checks the secondary diagonal
                    if (gameState.Board[i][2 - i] == move.Player)
                        rightDiagonal++;
                }

                // If we have a winner: change the state
                if (row == RowMaxSize || col == ColumnMaxSize ||
                    leftDiagonal == RowMaxSize || rightDiagonal == RowMaxSize)
                {
                    if (move.Player == ConstData.XString)
                    {
                        gameState.CurrentState = TicTacToeState.XPlayerWin;
                    }
                    else
                    {
                        gameState.CurrentState = TicTacToeState.OPlayerWin;
                    }
                    isGameOver = true;
                }

                // If all the board is full and no one wins then there is a tie
                if (gameState.StepsMade == TotalCellCount && !isGameOver)
                {
                    gameState.CurrentState = TicTacToeState.Tie;
                    isGameOver = true;
                }
            }

            // If there is no winner and the board is not full, change the active player
            if (!isGameOver)
            {
                if (gameState.CurrentState == TicTacToeState.XPlayerTurn)
                {
                    gameState.CurrentState = TicTacToeState.OPlayerTurn;
                }
                else
                {
                    gameState.CurrentState = TicTacToeState.XPlayerTurn;
                }
            }
            move.GameFlow = gameState.CurrentState.ToString();
        }

        /// <summary>
        /// Check if a move is legal.
        /// </summary>
        /// <param name="move">Move the validate.</param>
        /// <returns>True if the move is valid and false otherwise.</returns>
        private bool ValidateTicTacToeMove(TicTacToeMove move)
        {
            if (gameState.StepsMade == 0)
            {
                if(move.Player == ConstData.XString)
                {
                    gameState.CurrentState = TicTacToeState.XPlayerTurn;
                }
                else
                {
                    gameState.CurrentState = TicTacToeState.OPlayerTurn;
                }
            }
            bool isValid = true;
            // Invalidate the move if it is illegal because of the game state
            switch (gameState.CurrentState)
            {
                case TicTacToeState.XPlayerTurn:
                    if (move.Player == ConstData.OString )
                    {
                        isValid = false;
                    }
                    break;
                case TicTacToeState.OPlayerTurn:
                    if (move.Player == ConstData.XString)
                    {
                        isValid = false;
                    }
                    break;
                case TicTacToeState.XPlayerWin:
                case TicTacToeState.OPlayerWin:
                case TicTacToeState.Tie:
                    isValid = false;
                    break;
                default:
                    break;
            }

            // Check if the requested cell is off the board
            if (move.X < 0 || move.X > ColumnMaxSize || move.Y < 0 || move.Y > RowMaxSize)
            {
                isValid = false;
            }

            // Checks that the cell is empty
            if (gameState.Board[move.X][move.Y] != string.Empty)
            {
                isValid = false;
            }
            return isValid;
        }

        /// <summary>
        /// Put a move on the board.
        /// </summary>
        /// <param name="move">Move to perform.</param>
        private void MakeMove(TicTacToeMove move)
        {
            gameState.Board[move.X][move.Y] = move.Player;
        }

        /// <summary>
        /// XML Serialize an object and return it as a byte array.
        /// The method returns a byte array because this is the format that the raw message works with.
        /// </summary>
        /// <typeparam name="T">The type of the object.</typeparam>
        /// <param name="obj">The object to serialize.</param>
        /// <returns>A byte array containing the object's Xml serialized string.</returns>
        private byte[] GetBytes<T>(T obj) where T : IXmlSerializable
        {
            XmlSerializer serializer = new XmlSerializer(typeof(Message));
            
            // Creates a message
            Message message = new Message();
            message.ContentType = (MessageContentType)Enum.Parse(typeof(MessageContentType), obj.GetType().Name);
            message.Body = obj;

            StringBuilder output = new StringBuilder();

            // Serializes the message
            XmlWriter writer = XmlWriter.Create(output);
            serializer.Serialize(writer, message);

            return Encoding.Unicode.GetBytes(output.ToString());
        }



        /// <summary>
        /// Send a raw notification message to the client.
        /// </summary>
        /// <param name="clientURI">Client Uri.</param>
        /// <param name="rawData">The data to send.</param>
        /// <returns>The message send operation result for the last message sent .</returns>
        private static MessageSendResult SendMessage(Uri clientURI, byte[] rawData)
        {
            // Send a raw message
            //
            // For more information on the WindowsPhone.Recipes.Push.Messages namespace,
            // and the the push notification implementation please refer this link:
            // http://msdn.microsoft.com/en-us/WP7TrainingCourse_UsingPushNotificationsLab
            //
            RawPushNotificationMessage message = new RawPushNotificationMessage(MessageSendPriority.High);
            message.RawData = rawData;
            MessageSendResult result = message.Send(clientURI);

            return result;        
        }


        #endregion
    }


}
