﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using PhotonHashTable = ExitGames.Client.Photon.Hashtable;

public class CircleScript : MonoBehaviour {

	public List<int> stormTimes;
	public GameObject smog;

	Transform circle;
	int damageRate = 1;
	int circleNum = 0;
	float startSize = 900f, circleSize;
	Vector2 lastPoints;
    PhotonView view;
    [SerializeField] CircleTimeDisplay circleTimeDisplay;

    public AudioManager audioManager;
    public AudioClip exitTheCircleNoise;
    public AudioClip circleClosingNoise;
    public AudioClip outsideTheCircleMusic;

	public GameGenerator generator;

    [Header("Debug")]
    [SerializeField] [ReadOnly] string m_debugDesignation = "[CircleScript] ";
    [SerializeField] bool m_enableDebugMessages = false;

    // Use this for initialization
    void Start () 
    {
        // Get the circle as a parent of this object
		circle = transform.parent;

        // Set the starting size as our current
		circleSize = startSize;

        // Get a reference to our photon view component
        view = GetComponent<PhotonView>();

        // Store a reference to our Game Manager
        GameObject obj = GameObject.FindGameObjectWithTag("Game Manager");

        // If there is one in the scene, get the CircleTimeDisplay script from the same gameobject
        if (obj)
            circleTimeDisplay = obj.GetComponent<CircleTimeDisplay>();
        else if (m_enableDebugMessages)
            Debug.LogWarning(m_debugDesignation + "Unable to locate an active Game Manager. Circle display will not update without it.");

        if (m_enableDebugMessages)
            Debug.Log(m_debugDesignation + "Start the circle!");

        // Wait to start the circle
        StartCoroutine(WaitAndStartCircle());
    }

    IEnumerator WaitAndStartCircle() 
    {
        // If we are not connected to a photon network...
        if (!PhotonNetwork.IsConnected) 
        {
            if (m_enableDebugMessages)
                Debug.Log(m_debugDesignation + "Not connected to Photon. Waiting for GridHandler to initialize...");

            // Wait for the GridHandler to initialize to allow building.
            while (GridHandler.initializing)
                yield return null;

            if (m_enableDebugMessages)
                Debug.Log(m_debugDesignation + "GridHandler Initialized");
        }
        // If we are connected to a photon network...
        else 
        {
            // Store a false value
            bool completed = false;

            // While the value is false and the generator has not started the match...
			while (!completed && !generator.startMatchForced) 
            {
                if (m_enableDebugMessages)
                    Debug.Log(m_debugDesignation + "Checking for match start...");

                // If the network is connected...
                if (PhotonNetwork.IsConnected)
                {
                    if (m_enableDebugMessages)
                        Debug.Log(m_debugDesignation + "Connected!");

                    // Get the current number of players in the room
                    Dictionary<int, Photon.Realtime.Player> players = PhotonNetwork.CurrentRoom.Players;

                    // Set completed to true
                    completed = true;

                    // For each player in our room...
                    foreach (KeyValuePair<int, Photon.Realtime.Player> player in players)
                    {
                        // Get the properties of that player and store it in a hashtable
                        PhotonHashTable playerTable = player.Value.CustomProperties;

                        // Create a property field
                        object prop;

                        // If we are able to store the player's Loading value in the property...
                        if (playerTable.TryGetValue("loading", out prop))
                        {
                            // ...and they are loading...
                            if ((bool)prop)
                            {
                                if (m_enableDebugMessages)
                                    Debug.Log (m_debugDesignation + "UserID " + player.Value.UserId + " not loaded yet. Wait for player to load first!");

                                // Not all players are done loading, so set the value again to false
                                completed = false;

                                // Wait for the next frame
                                yield return null;

                                // Break the loop and start from the top
                                break;
                            }
                        }
                    }
                }
                // If we are connected, just wait for the next frame and try again
                else
                {
                    if (m_enableDebugMessages)
                        Debug.Log(m_debugDesignation + "Not connected!");

                    yield return null;
                }
            }
        }
        
        // Start to shrink the circle
        // This only occurs once all players are no longer loading
        StartCoroutine(ShrinkCircle());
    }

	void OnTriggerEnter (Collider col) 
    {
        // Toggles off the purple "smog" effect when the camera exits the smog
		if (col.tag == "MainCamera") 
			smog.SetActive (false);

        // If the object is a player...
		if (col.tag == "Player") 
        {
            // Get the playerbase script from the object
            PlayerBase _pb = col.GetComponent<PlayerBase>();

            // If none was found, return
            if (!_pb)
                return;

            // If the player is a networked player
            if (!CheckIfPlayerIsMe(_pb.gameObject))
                return;

            // Switch music off
            audioManager.StopMusic();

            // Make it so that the player's InStorm value is false
            _pb.InStorm = false;
		}
			
	}

	void OnTriggerExit (Collider col)
    {
        // Toggles on the purple "smog" effect when the camera enters the smog
        if (col.tag == "MainCamera") 
			smog.SetActive (true);

        // If the collider was a player...
		if (col.tag == "Player") 
        {
            PlayerBase _pb = col.GetComponent<PlayerBase>();

            if (!_pb)
                return;

            if (!CheckIfPlayerIsMe(col.gameObject))
                return;

            // Play exiting save circle sounds
            audioManager.PlaySound2D(exitTheCircleNoise);
            audioManager.PlayMusic(outsideTheCircleMusic);

            // Start the InStorm coroutine
            StartCoroutine (PlayerInStorm (col.gameObject));
		}
	}

    /// <summary>
    /// Returns true if the given GameObject is a local instance
    /// </summary>
    /// <param name="player"></param>
    /// <returns></returns>
    bool CheckIfPlayerIsMe(GameObject player) 
    {
        PhotonView view = player.GetComponent<PhotonView>();

        if (view && !view.IsMine)
            return false;

        return true;
    }

    /// <summary>
    /// Increases the display, damage rate, and size of the circle
    /// </summary>
	void AdjustSize () 
    {
		circleNum++;
		damageRate++;
        if (m_enableDebugMessages)
            Debug.Log(m_debugDesignation + " " + circleSize);
		circleSize = circleSize / 2f;
	}

    IEnumerator ShrinkCircle () 
    {
        if (m_enableDebugMessages)
            Debug.Log(m_debugDesignation + "Shrinking time!");

        // While the number of circles is at or below 3
		while(circleNum <= 3) 
        {
            if (m_enableDebugMessages)
                Debug.Log(m_debugDesignation + "Circle " + circleNum + " has begun!");

            // If we are connected to a game, we are the master client and we have a photon view
            if (PhotonNetwork.IsConnected && PhotonNetwork.IsMasterClient && view) 
            {
                // send how long until the next circle (also works as the time it takes for the next one to close might want to update this)
                PhotonDebugger.IncrementNumberOfMessages("circle");
                view.RPC("SetStormTime", RpcTarget.OthersBuffered, (float)PhotonNetwork.Time + stormTimes[circleNum]);

                if (m_enableDebugMessages)
                    Debug.Log(m_debugDesignation + stormTimes[circleNum] + " seconds remaining for this circle");

                // If we have a display script, set the next storm time to the PhotonNetwork's time value plus the time alotted for this circle number
                if (circleTimeDisplay)
                    circleTimeDisplay.SetNextStormTime((float)PhotonNetwork.Time + stormTimes[circleNum]);
                
                // Wait for the amount of time alotted
                yield return new WaitForSecondsRealtime(stormTimes[circleNum]);

                // Get a 2D coordinate based off of the size of the circle multiplied by a random value
                // If the circle's scale is over 1200...
                Vector2 circlePoints = (circle.localScale.x > 1200) ? 
                    // Create a value between [0, 0] and [1, 1] multiplied by 281.8
                    Random.insideUnitCircle * 281.8f : 
                    // Otherwise, use a random 2D value multiplied by half of the local scale minus half of the current size value, added to the last position
                    // This means that we will always get a 2D position within the previous circle
                    (Random.insideUnitCircle * (circle.localScale.x / 2f - (circleSize / 2f)) + lastPoints);
                
                // RPC call to shrink the circle to the given point
                PhotonDebugger.IncrementNumberOfMessages("circle");
                view.RPC("StartCircleShrinkToPoints", RpcTarget.OthersBuffered, circlePoints);

                // Start our own coroutine in the same way and repeat from the top.
                yield return StartCoroutine(ShrinkCircleToPoints(circlePoints));
            }
            // If we are not connected to Photon...
            else if (!PhotonNetwork.IsConnected) 
            {
                // There's an enormous delay when starting in editor where it sets the next time
                // but it doesn't actually start running for a few seconds so the first circle comes faster for no reason
                // To counter this, the original developer decided to just do a quick yield return here
                yield return null;

                // Same update as above
                if (circleTimeDisplay)
                    circleTimeDisplay.SetNextStormTime((float)Time.unscaledTime + stormTimes[circleNum]);
                
                // Wait for the circle time to elapse
                yield return new WaitForSecondsRealtime(stormTimes[circleNum]);

                // Create a new set of circle points, like above. We simply don't have to make an RPC call due to being offline.
                Vector2 circlePoints = (circle.localScale.x > 1200) ? Random.insideUnitCircle * 281.8f : (Random.insideUnitCircle * (circle.localScale.x / 2f - (circleSize / 2f)) + lastPoints);
                
                // Start the shrink coroutine
                yield return StartCoroutine(ShrinkCircleToPoints(circlePoints));
            }
            else 
            {
                // In case the current master of the lobby drops from the game, this script remains active on each client
                // We check to see if we've been made the master once every 3 seconds
                yield return new WaitForSecondsRealtime(3.0f); 
            }
        }

        if (m_enableDebugMessages)
            Debug.Log(m_debugDesignation + "Final round! Shrinking to nothing!");

        // After 4 circles (0 -> 3) we shrink to nothing
        StartCoroutine(ShrinkCircleUntilNothingIsLeft());
    }

    /// <summary>
    /// Handles shrinking the circle to a given coordinate pair
    /// </summary>
    /// <param name="circlePoints"></param>
    /// <returns></returns>
    IEnumerator ShrinkCircleToPoints(Vector2 circlePoints)
    {
        float t = 0f;

        Vector3 startScale = circle.localScale;
        Vector3 startPos = circle.position;
        lastPoints = circlePoints;

        Vector3 newPos = new Vector3(circlePoints.x, -150f, circlePoints.y);

        if (circleTimeDisplay)
        {
            circleTimeDisplay.SetStormWaiting(false);

            if (PhotonNetwork.IsConnected && PhotonNetwork.IsMasterClient)
            {
                float timeToCircleCloses = 30f;

                if (circleNum < 4)
                {
                    timeToCircleCloses = stormTimes[circleNum + 1];
                }

                float nextStormTime = timeToCircleCloses + (PhotonNetwork.IsConnected ? (float)PhotonNetwork.Time : Time.unscaledTime);

                circleTimeDisplay.SetNextStormTime(nextStormTime);

                PhotonDebugger.IncrementNumberOfMessages("circle");
                view.RPC("SetStormTime", RpcTarget.OthersBuffered, nextStormTime);
            }
            else
            {
                float timeToCircleCloses = 30f;
                if (circleNum < 4)
                {
                    timeToCircleCloses = stormTimes[circleNum + 1];
                }
                float nextStormTime = timeToCircleCloses + (PhotonNetwork.IsConnected ? (float)PhotonNetwork.Time : Time.unscaledTime);
                circleTimeDisplay.SetNextStormTime(nextStormTime);
            }
        }
        audioManager.PlaySound2D(circleClosingNoise);
        while (t < 1)
        {
            if (circleNum < 4)
            {
                t += Time.deltaTime / stormTimes[circleNum + 1];
            }
            else
            {
                t += Time.deltaTime / 60f;
            }
            circle.localScale = Vector3.Lerp(startScale, new Vector3(circleSize, 100, circleSize), t);
            circle.position = Vector3.Lerp(startPos, newPos, t);
            yield return null;
        }

        if (circleTimeDisplay)
            circleTimeDisplay.SetStormWaiting(true);

        AdjustSize();
    }

    IEnumerator ShrinkCircleUntilNothingIsLeft()
    {
        while (circle.localScale.x > 0.01f)
        {
            if (PhotonNetwork.IsConnected && PhotonNetwork.IsMasterClient && view)
            {
                // send how long until the next circle (also works as the time it takes for the next one to close might want to update this)
                PhotonDebugger.IncrementNumberOfMessages("circle");
                view.RPC("SetStormTime", RpcTarget.OthersBuffered, (float)PhotonNetwork.Time + stormTimes[3]);

                if (circleTimeDisplay)
                    circleTimeDisplay.SetNextStormTime((float)PhotonNetwork.Time + stormTimes[3]);

                yield return new WaitForSecondsRealtime(stormTimes[3]);

                // send the next circle points 
                Vector2 circlePoints = (circle.localScale.x > 1200) ? Random.insideUnitCircle * 281.8f : (Random.insideUnitCircle * (circle.localScale.x / 2f - (circleSize / 2f)) + lastPoints);
                
                PhotonDebugger.IncrementNumberOfMessages("circle");
                view.RPC("StartCircleShrinkToPoints", RpcTarget.OthersBuffered, circlePoints);

                yield return StartCoroutine(ShrinkCircleToPoints(circlePoints));
            }
            else if (!PhotonNetwork.IsConnected)
            {
                // there's an enormous delay when starting in editor where it sets the next time
                // but it doesn't actually start running for a few seconds so the first circle comes faster for no reason
                // so just do a quick yield return here
                yield return null;

                if (circleTimeDisplay)
                    circleTimeDisplay.SetNextStormTime((float)Time.unscaledTime + stormTimes[3]);

                yield return new WaitForSecondsRealtime(stormTimes[3]);

                Vector2 circlePoints = (circle.localScale.x > 1200) ?
                    Random.insideUnitCircle * 281.8f :
                    (Random.insideUnitCircle * (circle.localScale.x / 2f - (circleSize / 2f)) + lastPoints);
               
                yield return StartCoroutine(ShrinkCircleToPoints(circlePoints));
            }
            else
            {
                // if not master just keep running the coroutine just in case you become the master
                // every three seconds check if you are the master
                yield return new WaitForSecondsRealtime(3.0f);
            }
        }
    }


    IEnumerator PlayerInStorm (GameObject player)
    {
		PlayerBase pBase = player.GetComponent<PlayerBase> ();

		pBase.InStorm = true;

		yield return new WaitForSeconds (0.5f);

		while (pBase.InStorm)
        {
            pBase.DoDamage(damageRate);

			yield return new WaitForSeconds (1f);
		}

		yield return null;
	}

    [PunRPC]
    public void SetStormTime(float currentStormTime) 
    {
        if (m_enableDebugMessages)
            Debug.Log(m_debugDesignation + "Setting Storm Time to " + currentStormTime);

        // make the timer appear with this 
        if(circleTimeDisplay)
            circleTimeDisplay.SetNextStormTime(currentStormTime);
    }

    [PunRPC]
    public void StartCircleShrinkToPoints(Vector2 circlePoints) {
        StartCoroutine(ShrinkCircleToPoints(circlePoints));
    }
}
