// ----------------------------------------------------------------------------
// <copyright file="PunTurnManager.cs" company="Exit Games GmbH">
//   PhotonNetwork Framework for Unity - Copyright (C) 2018 Exit Games GmbH
// </copyright>
// <summary>
//  Manager for Turn Based games, using PUN
// </summary>
// <author>developer@exitgames.com</author>
// ----------------------------------------------------------------------------

using System;
using System.Collections.Generic;

using UnityEngine;

using Photon.Realtime;

using ExitGames.Client.Photon;
using Hashtable = ExitGames.Client.Photon.Hashtable;

namespace Photon.Pun.UtilityScripts
{
    /// <summary>
    /// Pun turnBased Game manager.
    /// Provides an Interface (IPunTurnManagerCallbacks) for the typical turn flow and logic, between players
    /// Provides Extensions for Player, Room and RoomInfo to feature dedicated api for TurnBased Needs
    /// </summary>
	public class PunTurnManager : MonoBehaviourPunCallbacks, IOnEventCallback
    {
        
        /// <summary>
        /// External definition for better garbage collection management, used in ProcessEvent.
        /// </summary>
        Player sender;
        
        /// <summary>
        /// Wraps accessing the "turn" custom properties of a room.
        /// </summary>
        /// <value>The turn index</value>
        public int Turn
        {
            get { return PhotonNetwork.CurrentRoom.GetTurn(); }
            private set
            {

                _isOverCallProcessed = false;

                PhotonNetwork.CurrentRoom.SetTurn(value, true);
            }
        }


        /// <summary>
        /// The duration of the turn in seconds.
        /// </summary>
        public float TurnDuration = 20f;

        /// <summary>
        /// Gets the elapsed time in the current turn in seconds
        /// </summary>
        /// <value>The elapsed time in the turn.</value>
        public float ElapsedTimeInTurn
        {
            get { return ((float) (PhotonNetwork.ServerTimestamp - PhotonNetwork.CurrentRoom.GetTurnStart())) / 1000.0f; }
        }


        /// <summary>
        /// Gets the remaining seconds for the current turn. Ranges from 0 to TurnDuration
        /// </summary>
        /// <value>The remaining seconds fo the current turn</value>
        public float RemainingSecondsInTurn
        {
            get { return Mathf.Max(0f, this.TurnDuration - this.ElapsedTimeInTurn); }
        }


        /// <summary>
        /// Gets a value indicating whether the turn is completed by all.
        /// </summary>
        /// <value><c>true</c> if this turn is completed by all; otherwise, <c>false</c>.</value>
        public bool IsCompletedByAll
        {
            get { return PhotonNetwork.CurrentRoom != null && Turn > 0 && this.finishedPlayers.Count == PhotonNetwork.CurrentRoom.PlayerCount; }
        }

        /// <summary>
        /// Gets a value indicating whether the current turn is finished by me.
        /// </summary>
        /// <value><c>true</c> if the current turn is finished by me; otherwise, <c>false</c>.</value>
        public bool IsFinishedByMe
        {
            get { return this.finishedPlayers.Contains(PhotonNetwork.LocalPlayer); }
        }

        /// <summary>
        /// Gets a value indicating whether the current turn is over. That is the ElapsedTimeinTurn is greater or equal to the TurnDuration
        /// </summary>
        /// <value><c>true</c> if the current turn is over; otherwise, <c>false</c>.</value>
        public bool IsOver
        {
            get { return this.RemainingSecondsInTurn <= 0f; }
        }

        /// <summary>
        /// The turn manager listener. Set this to your own script instance to catch Callbacks
        /// </summary>
        public IPunTurnManagerCallbacks TurnManagerListener;


        /// <summary>
        /// The finished players.
        /// </summary>
        private readonly HashSet<Player> finishedPlayers = new HashSet<Player>();

        /// <summary>
        /// The turn manager event offset event message byte. Used internaly for defining data in Room Custom Properties
        /// </summary>
        public const byte TurnManagerEventOffset = 0;

        /// <summary>
        /// The Move event message byte. Used internaly for saving data in Room Custom Properties
        /// </summary>
        public const byte EvMove = 1 + TurnManagerEventOffset;

        /// <summary>
        /// The Final Move event message byte. Used internaly for saving data in Room Custom Properties
        /// </summary>
        public const byte EvFinalMove = 2 + TurnManagerEventOffset;

        // keep track of message calls
        private bool _isOverCallProcessed = false;

        #region MonoBehaviour CallBack


        void Start(){}

        void Update()
        {
            if (Turn > 0 && this.IsOver && !_isOverCallProcessed)
            {
                _isOverCallProcessed = true;
                this.TurnManagerListener.OnTurnTimeEnds(this.Turn);
            }

        }

        #endregion


        /// <summary>
        /// Tells the TurnManager to begins a new turn.
        /// </summary>
        public void BeginTurn()
        {
            Turn = this.Turn + 1; // note: this will set a property in the room, which is available to the other players.
        }


        /// <summary>
        /// Call to send an action. Optionally finish the turn, too.
        /// The move object can be anything. Try to optimize though and only send the strict minimum set of information to define the turn move.
        /// </summary>
        /// <param name="move"></param>
        /// <param name="finished"></param>
        public void SendMove(object move, bool finished)
        {
            if (IsFinishedByMe)
            {
                UnityEngine.Debug.LogWarning("Can't SendMove. Turn is finished by this player.");
                return;
            }

            // along with the actual move, we have to send which turn this move belongs to
            Hashtable moveHt = new Hashtable();
            moveHt.Add("turn", Turn);
            moveHt.Add("move", move);

            byte evCode = (finished) ? EvFinalMove : EvMove;
            PhotonNetwork.RaiseEvent(evCode, moveHt, new RaiseEventOptions() {CachingOption = EventCaching.AddToRoomCache}, SendOptions.SendReliable);
            if (finished)
            {
                PhotonNetwork.LocalPlayer.SetFinishedTurn(Turn);
            }

            // the server won't send the event back to the origin (by default). to get the event, call it locally
            // (note: the order of events might be mixed up as we do this locally)
			ProcessOnEvent(evCode, moveHt, PhotonNetwork.LocalPlayer.ActorNumber);
        }

        /// <summary>
        /// Gets if the player finished the current turn.
        /// </summary>
        /// <returns><c>true</c>, if player finished the current turn, <c>false</c> otherwise.</returns>
        /// <param name="player">The Player to check for</param>
        public bool GetPlayerFinishedTurn(Player player)
        {
            if (player != null && this.finishedPlayers != null && this.finishedPlayers.Contains(player))
            {
                return true;
            }

            return false;
        }

        #region Callbacks

		// called internally
		void ProcessOnEvent(byte eventCode, object content, int senderId)
		{
            if (senderId == -1)
            {
                return;
            }
            
            sender = PhotonNetwork.CurrentRoom.GetPlayer(senderId);
            
            switch (eventCode)
			{
			case EvMove:
				{
					Hashtable evTable = content as Hashtable;
					int turn = (int)evTable["turn"];
					object move = evTable["move"];
					this.TurnManagerListener.OnPlayerMove(sender, turn, move);

					break;
				}
			case EvFinalMove:
				{
					Hashtable evTable = content as Hashtable;
					int turn = (int)evTable["turn"];
					object move = evTable["move"];

					if (turn == this.Turn)
					{
						this.finishedPlayers.Add(sender);

						this.TurnManagerListener.OnPlayerFinished(sender, turn, move);

					}

					if (IsCompletedByAll)
					{
						this.TurnManagerListener.OnTurnCompleted(this.Turn);
					}
					break;
				}
			}
		}

        /// <summary>
        /// Called by PhotonNetwork.OnEventCall registration
        /// </summary>
		/// <param name="photonEvent">Photon event.</param>
		public void OnEvent(EventData photonEvent)
        {
			this.ProcessOnEvent(photonEvent.Code, photonEvent.CustomData, photonEvent.Sender);
        }

        /// <summary>
        /// Called by PhotonNetwork
        /// </summary>
        /// <param name="propertiesThatChanged">Properties that changed.</param>
        public override void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
        {

            //   Debug.Log("OnRoomPropertiesUpdate: "+propertiesThatChanged.ToStringFull());

            if (propertiesThatChanged.ContainsKey("Turn"))
            {
                _isOverCallProcessed = false;
                this.finishedPlayers.Clear();
                this.TurnManagerListener.OnTurnBegins(this.Turn);
            }
        }

        #endregion
    }


    public interface IPunTurnManagerCallbacks
    {
        /// <summary>
        /// Called the turn begins event.
        /// </summary>
        /// <param name="turn">Turn Index</param>
        void OnTurnBegins(int turn);

        /// <summary>
        /// Called when a turn is completed (finished by all players)
        /// </summary>
        /// <param name="turn">Turn Index</param>
        void OnTurnCompleted(int turn);

        /// <summary>
        /// Called when a player moved (but did not finish the turn)
        /// </summary>
        /// <param name="player">Player reference</param>
        /// <param name="turn">Turn Index</param>
        /// <param name="move">Move Object data</param>
        void OnPlayerMove(Player player, int turn, object move);

        /// <summary>
        /// When a player finishes a turn (includes the action/move of that player)
        /// </summary>
        /// <param name="player">Player reference</param>
        /// <param name="turn">Turn index</param>
        /// <param name="move">Move Object data</param>
        void OnPlayerFinished(Player player, int turn, object move);


        /// <summary>
        /// Called when a turn completes due to a time constraint (timeout for a turn)
        /// </summary>
        /// <param name="turn">Turn index</param>
        void OnTurnTimeEnds(int turn);
    }


    public static class TurnExtensions
    {
        /// <summary>
        /// currently ongoing turn number
        /// </summary>
        public static readonly string TurnPropKey = "Turn";

        /// <summary>
        /// start (server) time for currently ongoing turn (used to calculate end)
        /// </summary>
        public static readonly string TurnStartPropKey = "TStart";

        /// <summary>
        /// Finished Turn of Actor (followed by number)
        /// </summary>
        public static readonly string FinishedTurnPropKey = "FToA";

        /// <summary>
        /// Sets the turn.
        /// </summary>
        /// <param name="room">Room reference</param>
        /// <param name="turn">Turn index</param>
        /// <param name="setStartTime">If set to <c>true</c> set start time.</param>
        public static void SetTurn(this Room room, int turn, bool setStartTime = false)
        {
            if (room == null || room.CustomProperties == null)
            {
                return;
            }

            Hashtable turnProps = new Hashtable();
            turnProps[TurnPropKey] = turn;
            if (setStartTime)
            {
                turnProps[TurnStartPropKey] = PhotonNetwork.ServerTimestamp;
            }

            room.SetCustomProperties(turnProps);
        }

        /// <summary>
        /// Gets the current turn from a RoomInfo
        /// </summary>
        /// <returns>The turn index </returns>
        /// <param name="room">RoomInfo reference</param>
        public static int GetTurn(this RoomInfo room)
        {
            if (room == null || room.CustomProperties == null || !room.CustomProperties.ContainsKey(TurnPropKey))
            {
                return 0;
            }

            return (int) room.CustomProperties[TurnPropKey];
        }


        /// <summary>
        /// Returns the start time when the turn began. This can be used to calculate how long it's going on.
        /// </summary>
        /// <returns>The turn start.</returns>
        /// <param name="room">Room.</param>
        public static int GetTurnStart(this RoomInfo room)
        {
            if (room == null || room.CustomProperties == null || !room.CustomProperties.ContainsKey(TurnStartPropKey))
            {
                return 0;
            }

            return (int) room.CustomProperties[TurnStartPropKey];
        }

        /// <summary>
        /// gets the player's finished turn (from the ROOM properties)
        /// </summary>
        /// <returns>The finished turn index</returns>
        /// <param name="player">Player reference</param>
        public static int GetFinishedTurn(this Player player)
        {
            Room room = PhotonNetwork.CurrentRoom;
            if (room == null || room.CustomProperties == null || !room.CustomProperties.ContainsKey(TurnPropKey))
            {
                return 0;
            }

            string propKey = FinishedTurnPropKey + player.ActorNumber;
            return (int) room.CustomProperties[propKey];
        }

        /// <summary>
        /// Sets the player's finished turn (in the ROOM properties)
        /// </summary>
        /// <param name="player">Player Reference</param>
        /// <param name="turn">Turn Index</param>
        public static void SetFinishedTurn(this Player player, int turn)
        {
            Room room = PhotonNetwork.CurrentRoom;
            if (room == null || room.CustomProperties == null)
            {
                return;
            }

            string propKey = FinishedTurnPropKey + player.ActorNumber;
            Hashtable finishedTurnProp = new Hashtable();
            finishedTurnProp[propKey] = turn;

            room.SetCustomProperties(finishedTurnProp);
        }
    }
}