using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Networking;
using Networking.Server;
using Networking;
using UnityEngine.SceneManagement;


public class GameManager : MonoBehaviour
{
    #region Inspector Field
    [Header("Settings")]
    [SerializeField]
    private float AnimationTime;
    [SerializeField]
    private GameModeReference CurrentGameMode;


    [Header("References")]
    [SerializeField]
    [Tooltip("Prefab of character for players to play")]
    private Character characterPrefab;

    [SerializeField]
    private ServerObject server;

    [SerializeField]
    private ClientList ClientList;

    #endregion Inspector Field

    #region Private Variables
    private Dictionary<int, PlayerData> playerData;
    #endregion Private Variables

    #region Read Only
    /// <summary>
    /// Easy access to IEnumerable in playerData so we can Enumerate through it
    /// </summary>
    private PlayerData[] playerDataAsArray { get { return playerData.Values.ToArray(); } }

    /// <summary>
    /// Easy access to GameMode value in CurrentGameMode reference
    /// </summary>
    private GameMode gameMode {get { return CurrentGameMode.Value; } }
    #endregion Read Only

    #region Unity Functions

    private void Start()
    {
        //Start Game
        StartCoroutine(GameRoutine());
    }

    private void Update()
    {
        //This is required so that the server can continue to recieve client messages
        //(it is a unity thing)
        server.ServerUpdate();
    }

    private void OnEnable()
    {
        //Let Server know we want to recieve some messages
        RegisterHandlers();
    }

    private void OnDisable()
    {
        //Let server know to not send messages this way
        UnRegisterHandlers();
    }
    #endregion Unity Functions

    #region Class Functions
    private IEnumerator GameRoutine()
    {

        //Allows game mode to instantiate anything it might need;
        gameMode.PreGameStart();

        //Spawn Characters and tell let the GameMode do anything with the characters it might want
        SpawnCharacters();
        playerDataAsArray.ForEach(p => p.client.SendLives());
        gameMode.GameStart(playerDataAsArray);

        //Loop until the GameMode lets us know the game is over
        while (!gameMode.isGameOver(playerDataAsArray))
        {

            //wait until we have recieved all player input
            yield return StartCoroutine(WaitForPlayerInput());

            //Routine for players movement
            yield return StartCoroutine(RoundRoutine()); //it's pretty long so it gets it's own coroutine;
        }

        //Let the gamemode know that the game is over
        gameMode.GameEnd(playerDataAsArray);
    }

    void removePlayer(PlayerData player)
    {
        player.client.ChangeScene("WaitScene");
        player.isDead = true;
    }

    private IEnumerator RoundRoutine()
    {
        //Tell the gamemode that we are starting a round
        gameMode.RoundStart(playerDataAsArray);

        //Loop until all players have finished moving
        while (playerDataAsArray.Any(p => !p.blockReader.Finished))
        {

            //Loop through all players
            foreach (PlayerData player in playerDataAsArray)
            {
                yield return StartCoroutine(MoveRoutine(player));//Move Player
                gameMode.PlayerMoved(player);//LetGameModeKnow
            }

            //Let Gamemode know all players have moved
            gameMode.AllPlayersMoved(playerDataAsArray.ToArray());
            //decrease lives here!!
            foreach(PlayerData player in playerDataAsArray)
            {
                gameMode.PlayerKilled(player);
            }

            playerDataAsArray.ForEach(p => p.client.SendLives()); //Update the players score

            //if Game is over break out of loop
            if (gameMode.isGameOver(playerDataAsArray))
                break;
        }

        //Let GameMode know that Round is Over
        gameMode.RoundEnd(playerDataAsArray.ToArray());


        //check is anyone has 0 lives remaining  
        //remove them from the array
        //force them back to the waiting for players screen
        foreach (PlayerData player in playerDataAsArray)
        {
            if(player.client.Lives == 0)
            {
                Debug.Log("Remove: " + player.client.characterAnimal);
                removePlayer(player);
            }
        }
        

            //Reset some player Data
        foreach (PlayerData player in playerDataAsArray)
        {
            if(player.client.Lives > 0)
            {
                player.blockReader.Reset();
                player.client.SendInventory();
            }
        }
    }

    private IEnumerator WaitForPlayerInput()
    {
        //send round length to players
        //#TODO make this only happen after first input
        LogicProtocols.FloatMsg RoundTime = new LogicProtocols.FloatMsg(gameMode.GetRoundTime());
        
        //playerDataAsArray.ForEach(p => p.client.conn.Send(LogicProtocols.SendRoundTime, RoundTime));
        foreach (PlayerData player in playerDataAsArray)
        {
            if (player.client.Lives > 0)
            {
                player.client.conn.Send(LogicProtocols.SendRoundTime, RoundTime);
            }
        }

        //Send players to input Scene
        //ClientList.ForEach(p => p.ChangeScene("ClientScene"));
        foreach (ClientData client in ClientList)
        {
            if(client.Lives > 0){client.ChangeScene("ClientScene");}
        }

        //Let gamemode know clients are input-ing
        gameMode.InputStart(playerDataAsArray);

        //wait for all players to 
        yield return new WaitUntil(() => playerData.All(p => p.Value.recievedList || p.Value.isDead));

        //reset
        //playerDataAsArray.ForEach(p => p.recievedList = false); //reset all players list
        foreach (PlayerData player in playerDataAsArray)
        {
            if (player.client.Lives > 0){player.recievedList = false;}
        }

        //Let gamemode know all inputs have been recieved
        gameMode.InputEnd(playerDataAsArray);
    }

    private IEnumerator MoveRoutine(PlayerData data)
    {
        bool blockFinished = false;
        float waitTime;

        //Loop until the current block indicates or the reader indicates it has finished
        while (!blockFinished && !data.blockReader.Finished)
        {

            //If the current block hasn't already been removed, remove it from the player inventory
            //We need to check if it has already been removed so loops don't eat an entire stack
            if (data.blockReader.CurrentBlock != null && !data.blockReader.CurrentBlock.hasBeenRemoved)
            {
                data.client.Inventory.Remove(data.blockReader.CurrentBlock);
                data.blockReader.CurrentBlock.hasBeenRemoved = true;
            }

            //Process the move
            blockFinished = data.blockReader.Read(data.character, AnimationTime, out waitTime);

            //Wait for the animation to finish
            yield return new WaitForSeconds(waitTime);

        }
    }

    private void SpawnCharacters()
    {
        playerData = new Dictionary<int, PlayerData>();
        Block[] SpawnBlocks = FindObjectsOfType<Block>().Where(p => p.isSpawnable).ToArray();

        int blockIndex = 0;
        foreach (Block block in SpawnBlocks)
        {
            Debug.Log("Block #" + blockIndex++ + " (" + block.transform.position.x + ", " + block.transform.position.y + ", " + block.transform.position.z + ")");
        }

        //int spawnIndex = 0;
        //If we have an odd number of players, then we start at spawn point 0 (in the middle)
        //If we have an even number, then we skip it
        //int spawnIndex = ((ClientList.Count() + 1) % 2);

        int spawnIndex = 0;

        foreach (ClientData client in ClientList)
        {
            //Debug.Log("spawnIndex = " + spawnIndex);
            Character newChar = Instantiate(characterPrefab);
            //Block startingBlock = SpawnBlocks[(spawnIndex++ % ClientList.ConnectedClients.Count)];
            Block startingBlock = SpawnBlocks[spawnIndex++];
            newChar.Initialise(startingBlock, client.Inventory, client.characterAnimal);
            newChar.transform.forward = startingBlock.SpawnDirection.ToVector();
            playerData.Add(client.ID, new PlayerData(newChar, client));
            newChar.ClientLink = client;
            client.playerCharacter = newChar;
        }
    } 
    #endregion Class Functions

    #region Networking Functions

    /// <summary>
    /// Registers functions which should deal with incoming messages from clients
    /// </summary>
    private void RegisterHandlers()
    {
        server.server.RegisterHandler(LogicProtocols.SendLogicList, RecieveLogicList);
    }

    /// <summary>
    /// Clears any functions from server register which the manager would deal with
    /// </summary>
    private void UnRegisterHandlers()
    {
        server.server.UnregisterHandler(LogicProtocols.SendLogicList);
    }

    /// <summary>
    /// Called when server recieves moves from client
    /// </summary>
    /// <param name="msg">messages passed by server</param>
    private void RecieveLogicList(NetworkMessage msg)
    {

        //try to read base message as a logic message
        LogicProtocols.LogicMsg logicMsg;
        if (!msg.TryRead(out logicMsg))
            return;

        //Debug that we have recieved it
        Debug.Log("Recieved function from " + ClientList[msg.conn.connectionId].Name);

        //Update player Data with recieved list
        playerData[msg.conn.connectionId].blockReader.LogicChain = new List<LogicBlock>(logicMsg.elements);
        playerData[msg.conn.connectionId].recievedList = true;
    }

    #endregion Networking Functions   
}

public class PlayerData
{
    public Character character;
    public BlockReader blockReader;
    public ClientData client;

    public bool recievedList;
    public bool isDead = false;

    public PlayerData(Character character, ClientData client)
    {
        this.character = character;
        this.client = client;
        blockReader = new BlockReader();
    }
}
