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;

    public blockSpawn spawnBlock;

    #endregion Inspector Field

    #region Private Variables
    public Dictionary<int, PlayerData> playerData;
    private ActiveBlock[] EnvironmentBlocks;
    #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()
    {
        spawnBlock = gameObject.GetComponent<blockSpawn>();
        
        //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();

        //gets list of items to check spawn locations
        spawnBlock.wakeup();

        //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))
        {
            int playersalive_left = 0;
            int spotinarray = 0;
            int players = 0;
            //check here if one player left
            foreach(PlayerData data in playerDataAsArray)
            {
                players++;
                if (data.character.lives > 0)
                {
                    playersalive_left++;
                    spotinarray = players;
                }
            }
            /*if(playersalive_left == 1)
            {
                GlobalVariables.winneranimal = playerDataAsArray[spotinarray].client.characterAnimal;
                GlobalVariables.winnername = playerDataAsArray[spotinarray].client.Name;
                SceneManager.LoadScene("Winner");
            }*/

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

            //I hate having to do this
            EnvironmentBlocks = FindObjectsOfType<ActiveBlock>();
            ////Debug.Log("Active blocks found: " + EnvironmentBlocks.Length);

            //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);
    }

    private 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);

            //I hate having to do this
            EnvironmentBlocks = FindObjectsOfType<ActiveBlock>();

            //Let the environment take a turn
            yield return StartCoroutine(EnvironmentTurn());
            gameMode.EnvironmentTurn(playerDataAsArray);

            //if Game is over break out of loop
            if (gameMode.isGameOver(playerDataAsArray))
                break;
        }
        //decrease lives here!!
        foreach (PlayerData player in playerDataAsArray)
        {
            gameMode.PlayerKilled(player);
        }

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

        //Let GameMode know that Round is Over
        yield return StartCoroutine(EnvironmentEnd());
        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);
            }
        }
        //spawn collectible logic blocks
        spawnBlock.Spawn();


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

        playerData = playerData.OrderBy(unit => unit.Value.character.CurrentBlock.transform.position.x).ToDictionary(unit => unit.Key, unit => unit.Value);
        int order = 1;
        foreach (PlayerData data in playerDataAsArray)
        {
            data.character.runOrder = order;
            order++;
        }
    }

    private IEnumerator WaitForPlayerInput()
    {
        //send round length to players
        //#TODO make this only happen after first input
        LogicProtocols.FloatMsg RoundTime = new LogicProtocols.FloatMsg(gameMode.GetRoundTime());

        foreach (PlayerData player in playerDataAsArray)
        {
            if (player.client.Lives == 0)
            {
                //Debug.Log("Remove: " + player.client.characterAnimal);
                removePlayer(player);
            }
        }

        //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 || p.Value.character.lives == 0));

        //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)
    {
        //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);

        yield return StartCoroutine(data.blockReader.Read(data.character, AnimationTime));
    }

    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;
        }
    }

    private IEnumerator EnvironmentTurn()
    {
        foreach (var InitiativeGroup in EnvironmentBlocks.GroupBy(p => p.GetInitative())){
            
            foreach (ActiveBlock block in InitiativeGroup)
                StartCoroutine(block.OnEnvironmentTurn(playerDataAsArray));

            yield return new WaitUntil(() => InitiativeGroup.All(p => p.isFinished));
            InitiativeGroup.ForEach(p => p.Reset());
        }

    }

    private IEnumerator EnvironmentEnd()
    {
        foreach (var InitiativeGroup in EnvironmentBlocks.GroupBy(p => p.GetInitative())){

            foreach (ActiveBlock block in InitiativeGroup)
                StartCoroutine(block.OnRoundEnd(playerDataAsArray));

            yield return new WaitUntil(() => InitiativeGroup.All(p => p.isFinished));
            InitiativeGroup.ForEach(p => p.Reset());
        }
    }

    private ActiveBlock[] GetActiveBlocksOf(int intiative)
    {
        return EnvironmentBlocks.Where(p => p.GetInitative() == intiative).ToArray();
    }
    #endregion Class Functions

    #region Networking Functions

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

    /// <summary>
    /// Clears any functions from server register which the manager would deal with
    /// </summary>
    private void UnRegisterHandlers()
    {
        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.Hash()].blockReader.LogicChain = new List<LogicBlock>(logicMsg.elements);
        playerData[msg.conn.Hash()].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();
    }
}
