using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Networking.Server;
using TMPro;
using UnityEngine.UI;
using System.Linq;

[CreateAssetMenu(menuName = "Major Project/GameModes/Racetrack", order = 201)]
public class RacetrackGameMode : GameMode
{
    public MapManager mapManager;
    public blockSpawn spawn;

    public int MaxRound = 999;
    public string nextScene = "ServerTestScene";
    List<ClientData> ConnectedClients;
    public Material OverlayMaterial;
    public float scrollSpeed = 1.0f; //The rate at which the level will scroll past

    [SerializeField]
    [Tooltip("Layers to ignore when checking for blocks")]
    public LayerMask Ignore;


    public int RoundCount { get; private set; }
    private Dictionary<ClientData, List<Block>> BlocksOwned;
    int currentBoulderCount;

    /// <summary>
    /// Called once before any players have spawned
    /// </summary>
    protected override void OnPreGameStart()
    {
        mapManager.init();
    }

    /// <summary>
    /// Called once all players have finished their moves but before the Objective is checked
    /// </summary>
    protected override void OnRoundEnd(PlayerData[] allPlayers)
    {
        cameraCheck(allPlayers);

        //At the end of each round, any stuck players are freed to resume moving next round
        respawnPlayers(allPlayers);

        /*foreach (PlayerData player in allPlayers)
        {
            player.character.stuck = false;

            if (player.character.respawnNeeded && player.client.Lives > 0)
            {
                player.character.respawnCharacter();
            }
        }*/

        RoundCount++;
    }

    void respawnPlayers(PlayerData[] allPlayers)
    {
        //First step: determine if anyone is waiting to respawn
        List<PlayerData> respawningPlayers = new List<PlayerData>();
        List<PlayerData> survivingPlayers = new List<PlayerData>();
        foreach (PlayerData player in allPlayers)
        {
            if (player.client.Lives > 0)
            {
                if (player.character.respawnNeeded)
                {
                    respawningPlayers.Add(player);
                }
                else
                {
                    survivingPlayers.Add(player);
                }
            }
        }

        //If nobody needs a respawn, we stop here. Otherwise, we carry on
        if (respawningPlayers.Count > 0)
        {
            //Next step: figure out the appropriate respawn point
            //If there are any players left, it will be the average of their positions
            //Otherwise, it will be as close to the centre of the screen as possible
            Vector3 respawnLocation = new Vector3(0.0f, -0.5f, 0.0f);
            //Debug.Log("Initial respawn location: " + respawnLocation);

            if (survivingPlayers.Count > 0) //If any players are not currently waiting to respawn, we get the average of their locations
            {
                //Debug.Log("Averaging survivor locations");
                //We list the positions of all the surviving players
                List<Vector3> survivingPlayerLocations = new List<Vector3>();
                foreach (PlayerData player in survivingPlayers)
                {
                    survivingPlayerLocations.Add(player.character.transform.position);
                }

                foreach (Vector3 vec in survivingPlayerLocations)
                {
                    respawnLocation += vec;
                }

                respawnLocation = respawnLocation / survivingPlayers.Count;
                //Debug.Log("Averaged survivor locations, new respawn location: " + respawnLocation);
            }
            else //Otherwise, we go for the middle of the screen
            {
                //Debug.Log("Seeking the middle of the screen");
                //We start in the middle of the track, at the back, and scroll forward until we're past the centre of the camera's view
                respawnLocation.x = mapManager.startX;

                Vector3 respawnLocationVP = Camera.main.WorldToViewportPoint(respawnLocation);

                while (respawnLocationVP.x < 0.5f || respawnLocationVP.y < 0.5f)
                {
                    respawnLocation.x += 1.0f;
                    respawnLocationVP = Camera.main.WorldToViewportPoint(respawnLocation);
                    //Debug.Log("respawnLocation = " + respawnLocation + ", viewport = " + respawnLocationVP);
                }
                //Debug.Log("Found middle of camera view, new respawn location: " + respawnLocation);
            }

            //Now we randomly pick blocks in the vicinity of this point to respawn players on
            //Valid blocks are those within x & +/-1 from the chosen location
            //If we don't find enough open, unoccupied blocks within that radius, then we widen it

            int radius = 0;
            List<Block> respawnBlocks;
            do
            {
                respawnBlocks = new List<Block>();
                radius++;

                //Debug.Log("Finding respawn blocks at radius = " + radius);

                for (int i = -1 * radius; i <= radius; i++)
                {
                    for (int j = -1 * radius; j <= radius; j++)
                    {
                        Block currentBlock;

                        Vector3 currentPos = new Vector3(respawnLocation.x + i, respawnLocation.y, respawnLocation.z + j);

                        //Debug.Log("Checking position " + currentPos);

                        if (Block.isBlockAtPosition(currentPos, 1, ~Ignore, out currentBlock))
                        {
                            /*Debug.Log("Found block at " + currentPos
                                    + ". is_Walkable = " + currentBlock.is_Walkable
                                    + ", currentBlock is Water = " + (currentBlock is Water)
                                    + ", isPit = " + currentBlock.isPit
                                    + ", no currentPlayer = " + (currentBlock.CurrentPlayer == null));*/

                            if (currentBlock.is_Walkable //Are we allowed on this block?
                            && !(currentBlock is Water) && !(currentBlock.isPit) //Don't respawn on top of instant traps
                            && currentBlock.CurrentPlayer == null) //Block must be unoccupied
                            {
                                respawnBlocks.Add(currentBlock);
                                //Debug.Log("Added block at " + currentPos);
                            }
                            else
                            {
                                //Debug.Log("Block at " + currentPos + " invalid");
                            }
                        }
                        else
                        {
                            //Debug.Log("No block at " + currentPos);
                        }
                    }
                }
            } while (respawnBlocks.Count < respawningPlayers.Count);

            //Debug.Log("Possible respawn spots: ");

            /*foreach(Block block in respawnBlocks)
            {
                Debug.Log(block.transform.position);
            }*/

            foreach (PlayerData player in respawningPlayers)
            {
                //We randomly pick a block for them to respawn on
                int respawnIndex = (int)Random.Range(0.0f, (float)respawnBlocks.Count);
                //Debug.Log("Respawn index = " + respawnIndex);
                Block respawnBlock = respawnBlocks[respawnIndex];
                //Debug.Log("Respawn location = " + respawnBlock.transform.position);
                player.character.respawnCharacter(respawnBlock);

                respawnBlocks.Remove(respawnBlock); //Then we remove it from the list for the next player
            }
        }
    }

    void cameraCheck(PlayerData[] allPlayers)
    {
        //Get the average x-position of all players
        float xAvg = 0.0f;
        int livePlayerCount = 0;

        foreach (PlayerData player in allPlayers)
        {
            if (!(player.character.respawnNeeded) && player.client.Lives > 0)
            {
                xAvg += player.character.transform.position.x;
                livePlayerCount++;
            }
        }

        if (livePlayerCount > 0)
        {
            xAvg = xAvg / livePlayerCount;
        }

        //Turn that position into a vector in viewport space
        Vector3 xAvgPos = new Vector3(xAvg, 0.0f, 0.0f);
        Vector3 xAvgVP = Camera.main.WorldToViewportPoint(xAvgPos);

        //Debug.Log("xAvgPos = " + xAvgPos + ", xAvgVP = " + xAvgVP);

        //We move the camera forward by at least one increment
        //Keep doing it until the average x-position is roughly centred
        do
        {
            Camera.main.transform.Translate(scrollSpeed, 0, 0, Space.World);
            xAvgVP = Camera.main.WorldToViewportPoint(xAvgPos);

            //Debug.Log("Camera = " + Camera.main.transform.position + ", xAvgVP = " + xAvgVP);
        } while (xAvgVP.x > 0.5f || xAvgVP.y > 0.5f);

        //If the foremost player is off the screen, scroll forward to catch up with them

        //Find who's furthest ahead
        PlayerData playerAhead = allPlayers[0];
        foreach (PlayerData player in allPlayers)
        {
            if (player.client.Lives > 0 && player.character.transform.position.x > playerAhead.character.transform.position.x)
            {
                playerAhead = player;
            }
        }

        //Turn their position to a viewport vector
        Vector3 playerVP = Camera.main.WorldToViewportPoint(playerAhead.character.transform.position);

        //If that position is not in view, advance until it is
        while (playerVP.x > 1 || playerVP.y > 1)
        {
            Camera.main.transform.Translate(scrollSpeed, 0, 0, Space.World);
            playerVP = Camera.main.WorldToViewportPoint(playerAhead.character.transform.position);
        }/**/

        //If anyone is out of view at this point, they are dead and need to respawn
        foreach (PlayerData player in allPlayers)
        {
            if (player.client.Lives > 0 && !(player.character.respawnNeeded))
            {
                playerVP = Camera.main.WorldToViewportPoint(player.character.transform.position);
                if (playerVP.x < 0.0f || playerVP.y < 0.0f)
                {
                    player.character.respawnNeeded = true;
                }
            }
        }

        //We check for track sections we need to add/remove
        mapManager.checkTrack();
        //spawn.wakeup();

        //Move the camera forward at a steady rate each round
        /*if (scrollSpeed > 0.0f)
        {
            Camera.main.transform.Translate(scrollSpeed, 0, 0, Space.World);
            mapManager.checkTrack();
            //Debug.Log("New camera position at x = " + Camera.main.transform.position.x);

            //Plan: get average position of surviving players, centre camera view on it
            //Also, will need to check every time someone moves: are they still on camera?
            //If ahead of camera, advance it until they're visible
            //If behind camera, they lose a life & respawn
        }
        else
        {
            //Debug.Log("Not scrolling");
        }*/

    }


    /// <summary>
    /// Checks if the Game is finished
    /// </summary>
    /// <returns>returns if game is finished</returns>
    public override bool isGameOver(PlayerData[] allPlayers)
    {
        return (RoundCount >= MaxRound -1);
    }


    /// <summary>
    /// Called once per player after they have moved onto a block
    /// </summary>
    /// <param name="character">Character which moved</param>
    /// <param name="client">Client of the character</param>
    /// <param name="currentBlock">Block moved onto</param>
    protected override void OnPlayerMoved(Character character, ClientData client, Block currentBlock)
    {
        handleFalling(character, client, currentBlock, character.justMoved);

        /*Debug.Log("Moved to square at " + currentBlock.transform.position.x + ", "
             + currentBlock.transform.position.y + ", "
              + currentBlock.transform.position.z);

        //If a character has fallen in the water or into a pit, we mark that fact, and they lose the rest of their turn
        character.inWater = currentBlock.isWater;
        character.respawnNeeded = currentBlock.isPit;

        if (character.inWater == true || character.respawnNeeded == true)
        {
            character.stuck = true;
        }

        Debug.Log("inWater = " + character.inWater + ", respawnNeeded = " + character.respawnNeeded + ", stuck = " + character.stuck);*/

        //Commented out because we don't do this in the racetrack mode

        /*ClientData OwnedClient;
        Material overlay = null;
        if (isOwned(currentBlock, out OwnedClient))
        {
            if (OwnedClient == client)
                return;

            BlocksOwned[OwnedClient].Remove(currentBlock);

            foreach (Material mat in currentBlock.GetComponent<Renderer>().materials)
            {
                if (mat.name == OverlayMaterial.name + " (Instance)")
                    overlay = mat;
            }
        }

        if (overlay == null)
        {
            overlay = new Material(OverlayMaterial);
            List<Material> mats = new List<Material>(currentBlock.GetComponent<Renderer>().materials);
            mats.Add(overlay);
            currentBlock.GetComponent<Renderer>().materials = mats.ToArray();
        }

        overlay.SetColor("_NewColor", client.Color);

        if (!BlocksOwned.ContainsKey(client))
            BlocksOwned.Add(client, new List<Block>());

        BlocksOwned[client].Add(currentBlock);
         
        if (overlay != null)
            currentBlock.StartCoroutine(AnimateBlock(overlay, 0.25f));*/
    }

    protected override void OnRoundStart(PlayerData[] allPlayers)
    {
        
    }

    protected override void OnAllPlayersMoved(PlayerData[] allPlayers)
    {
        foreach (PlayerData player in allPlayers)
        {
            /*  The justMoved variable is used to determine whether a player taking their turn in the water should become stuck
             *  (because they moved into/in the water), or not (because they're turning while remaining in the same square)
             *  It's not needed from move to move, so we clear it
                */
            player.character.justMoved = false;

           /* if (BlocksOwned.ContainsKey(player.client))
                player.client.SceneScore = BlocksOwned[player.client].Count;
            else
                player.client.SceneScore = 0;*/
        }
    }

    protected override void OnGameOver(PlayerData[] allPlayers)
    {
        throw new System.NotImplementedException();
    }

    private bool isOwned(Block block, out ClientData client)
    {
        client = null;

        foreach (KeyValuePair<ClientData, List<Block>> ownedList in BlocksOwned)
        {
            if (ownedList.Value.Contains(block))
            {
                client = ownedList.Key;
                return true;
            }
        }

        return false;
    }

    private void handleFalling(Character character, ClientData client, Block currentBlock, bool didMove)
    {
        //If a character has fallen in the water or into a pit, we mark that fact, and they lose the rest of their turn
        character.inWater = currentBlock.isWater;
        character.respawnNeeded = currentBlock.isPit;
        character.onCrystal = currentBlock.isCrystals;
        character.underRock = currentBlock.isRock;

        if (didMove && (character.inWater || character.respawnNeeded))
        {
            character.stuck = true;
        }
        //Debug.Log("inWater = " + character.inWater + ", respawnNeeded = " + character.respawnNeeded + ", stuck = " + character.stuck);
    }

    protected override void OnPlayerKilled(Character character, ClientData client)
    {
        if (character.respawnNeeded || character.onCrystal)
        {
            character.lives -= 1;
            character.ClientLink.Lives = character.lives;
        }
        if(character.underRock && currentBoulderCount < 1)
        {
            character.lives -= 1;
            character.ClientLink.Lives = character.lives;
        }
    }

    private IEnumerator AnimateBlock(Material mat, float time)
    {
        float timeElasped = 0;

        while (timeElasped < time)
        {
            mat.SetFloat("_Multiplier", (timeElasped / time));
            yield return new WaitForEndOfFrame();
            timeElasped += Time.deltaTime;
        }
        mat.SetFloat("_Multiplier", 1);
    }

    protected override void OnGameStart(PlayerData[] AllPlayers)
    {
        BlocksOwned = new Dictionary<ClientData, List<Block>>();
        RoundCount = 0;
        AllPlayers = getPlayerOrder(AllPlayers);
        for (int i = 0; i < AllPlayers.Length; i++)
        {
            OnPlayerMoved(AllPlayers[i].character, AllPlayers[i].client, AllPlayers[i].character.CurrentBlock);
        }
    }

    public override PlayerData[] getPlayerOrder(PlayerData[] AllPlayers)
    {
        AllPlayers = AllPlayers.ToArray().OrderBy(unit => unit.character.CurrentBlock.transform.position.x).ToArray();
        int order = 1;
        foreach(PlayerData data in AllPlayers)
        {
            data.character.runOrder = order;
            order++;
        }
        return AllPlayers;
    }
}
