// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using UnityEngine;
using UnityEngine.Networking;
using HoloToolkit.Unity.InputModule;

namespace HoloToolkit.Unity.SharingWithUNET
{
    /// <summary>
    /// Controls player behavior (local and remote).
    /// </summary>
    [NetworkSettings(sendInterval = 0.033f)]
    public class PlayerController : NetworkBehaviour, IInputClickHandler
    {
        private static PlayerController _Instance = null;
        /// <summary>
        /// Instance of the PlayerController that represents the local player.
        /// </summary>
        public static PlayerController Instance
        {
            get
            {
                return _Instance;
            }
        }

        /// <summary>
        /// The game object that represents the 'bullet' for 
        /// this player. Must exist in the spawnable prefabs on the
        /// NetworkManager.
        /// </summary>
        public GameObject bullet;

        public bool CanShareAnchors;

        /// <summary>
        /// The transform of the shared world anchor.
        /// </summary>
        private Transform sharedWorldAnchorTransform;

        /// <summary>
        /// The anchor manager.
        /// </summary>
        private UNetAnchorManager anchorManager;

        /// <summary>
        /// The position relative to the shared world anchor.
        /// </summary>
        [SyncVar]
        private Vector3 localPosition;

        /// <summary>
        /// The rotation relative to the shared world anchor.
        /// </summary>
        [SyncVar]
        private Quaternion localRotation;

        /// <summary>
        /// Sets the localPosition and localRotation on clients.
        /// </summary>
        /// <param name="postion">the localPosition to set</param>
        /// <param name="rotation">the localRotation to set</param>
        [Command(channel = 1)]
        public void CmdTransform(Vector3 postion, Quaternion rotation)
        {
            localPosition = postion;
            localRotation = rotation;
        }

        /// <summary>
        /// Tracks if the player associated with the script has found the shared anchor
        /// </summary>
        [SyncVar(hook = "AnchorEstablishedChanged")]
        bool AnchorEstablished;

        /// <summary>
        /// Sent from a local client to the host to update if the shared
        /// anchor has been found.
        /// </summary>
        /// <param name="Established">true if the shared anchor is found</param>
        [Command]
        private void CmdSendAnchorEstablished(bool Established)
        {
            AnchorEstablished = Established;
            if (Established && SharesSpatialAnchors && !isLocalPlayer)
            {
                Debug.Log("remote device likes the anchor");
#if UNITY_WSA
                anchorManager.AnchorFoundRemotely();
#endif
            }
        }

        /// <summary>
        /// Called when the anchor is either lost or found
        /// </summary>
        /// <param name="update">true if the anchor is found</param>
        void AnchorEstablishedChanged(bool update)
        {
            Debug.LogFormat("AnchorEstablished for {0} was {1} is now {2}", PlayerName, AnchorEstablished, update);
            AnchorEstablished = update;
            // only draw the mesh for the player if the anchor is found.
            GetComponentInChildren<MeshRenderer>().enabled = update;
        }

        /// <summary>
        /// Tracks the player name.
        /// </summary>
        [SyncVar(hook = "PlayerNameChanged")]
        string PlayerName;

        /// <summary>
        /// Called to set the player name
        /// </summary>
        /// <param name="playerName">The name to update to</param>
        [Command]
        private void CmdSetPlayerName(string playerName)
        {
            PlayerName = playerName;
        }

        /// <summary>
        /// Called when the player name changes.
        /// </summary>
        /// <param name="update">the updated name</param>
        void PlayerNameChanged(string update)
        {
            Debug.LogFormat("Player name changing from {0} to {1}", PlayerName, update);
            PlayerName = update;
            // Special case for spectator view
            if (PlayerName.ToLower() == "spectatorviewpc")
            {
                gameObject.SetActive(false);
            }
        }

#pragma warning disable 0414
        /// <summary>
        /// Keeps track of the player's IP address
        /// </summary>
        [SyncVar(hook = "PlayerIpChanged")]
        string PlayerIp;
#pragma warning restore 0414

        /// <summary>
        /// Called to set the Ip address
        /// </summary>
        /// <param name="playerIp"></param>
        [Command]
        private void CmdSetPlayerIp(string playerIp)
        {
            PlayerIp = playerIp;
        }

        /// <summary>
        /// Called when the player Ip address changes
        /// </summary>
        /// <param name="update">The updated IP address</param>
        void PlayerIpChanged(string update)
        {
            PlayerIp = update;
        }

        /// <summary>
        /// Tracks if the player can share spatial anchors
        /// </summary>
        [SyncVar(hook = "SharesAnchorsChanged")]
        public bool SharesSpatialAnchors;

        /// <summary>
        /// Called to update if the player can share spatial anchors.
        /// </summary>
        /// <param name="canShareAnchors">True if the device can share spatial anchors.</param>
        [Command]
        private void CmdSetCanShareAnchors(bool canShareAnchors)
        {
            Debug.Log("CMDSetCanShare " + canShareAnchors);
            SharesSpatialAnchors = canShareAnchors;
        }

        /// <summary>
        /// Called when the ability to share spatial anchors changes
        /// </summary>
        /// <param name="update">True if the device can share spatial anchors.</param>
        void SharesAnchorsChanged(bool update)
        {
            SharesSpatialAnchors = update;
            Debug.LogFormat("{0} {1} share", PlayerName, SharesSpatialAnchors ? "does" : "does not");
        }

        /// <summary>
        /// Script that handles finding, creating, and joining sessions.
        /// </summary>
        private NetworkDiscoveryWithAnchors networkDiscovery;

        void Awake()
        {
            networkDiscovery = NetworkDiscoveryWithAnchors.Instance;
            anchorManager = UNetAnchorManager.Instance;
        }

        private void Start()
        {
            if (SharedCollection.Instance == null)
            {
                Debug.LogError("This script required a SharedCollection script attached to a gameobject in the scene");
                Destroy(this);
                return;
            }

            if (isLocalPlayer)
            {
                // If we are the local player then we want to have airtaps 
                // sent to this object so that projeciles can be spawned.
                InputManager.Instance.AddGlobalListener(gameObject);
                InitializeLocalPlayer();
            }
            else
            {
                Debug.Log("remote player");
                GetComponentInChildren<MeshRenderer>().material.color = Color.red;
                AnchorEstablishedChanged(AnchorEstablished);
                SharesAnchorsChanged(SharesSpatialAnchors);
            }

            sharedWorldAnchorTransform = SharedCollection.Instance.gameObject.transform;
            transform.SetParent(sharedWorldAnchorTransform);
        }

        private void Update()
        {
            // If we aren't the local player, we just need to make sure that the position of this object is set properly
            // so that we properly render their avatar in our world.
            if (!isLocalPlayer && string.IsNullOrEmpty(PlayerName) == false)
            {
                transform.localPosition = Vector3.Lerp(transform.localPosition, localPosition, 0.3f);
                transform.localRotation = localRotation;
                return;
            }

            if (!isLocalPlayer)
            {
                return;
            }

            // if our anchor established state has changed, update everyone
            if (AnchorEstablished != anchorManager.AnchorEstablished)
            {
                CmdSendAnchorEstablished(anchorManager.AnchorEstablished);
            }

            // if our anchor isn't established, we shouldn't bother sending transforms.
            if (AnchorEstablished == false)
            {
                return;
            }

            // if we are the remote player then we need to update our worldPosition and then set our 
            // local (to the shared world anchor) position for other clients to update our position in their world.
            transform.position = CameraCache.Main.transform.position;
            transform.rotation = CameraCache.Main.transform.rotation;

            // For UNET we use a command to signal the host to update our local position
            // and rotation
            CmdTransform(transform.localPosition, transform.localRotation);
        }

        /// <summary>
        /// Sets up all of the local player information
        /// </summary>
        private void InitializeLocalPlayer()
        {
            if (isLocalPlayer)
            {
                Debug.Log("Setting instance for local player ");
                _Instance = this;
                Debug.LogFormat("Set local player name {0} ip {1}", networkDiscovery.broadcastData, networkDiscovery.LocalIp);
                CmdSetPlayerName(networkDiscovery.broadcastData);
                CmdSetPlayerIp(networkDiscovery.LocalIp);
#if UNITY_WSA
#if UNITY_2017_2_OR_NEWER
                CanShareAnchors = !UnityEngine.XR.WSA.HolographicSettings.IsDisplayOpaque;
#else
                CanShareAnchors = !Application.isEditor && UnityEngine.VR.VRDevice.isPresent;
#endif
#endif
                Debug.LogFormat("local player {0} share anchors ", (CanShareAnchors ? "does not" : "does"));
                CmdSetCanShareAnchors(CanShareAnchors);
            }
        }

        private void OnDestroy()
        {
            if (isLocalPlayer)
            {
                InputManager.Instance.RemoveGlobalListener(gameObject);
            }
        }

        /// <summary>
        /// Called when the local player starts.  In general the side effect should not be noticed
        /// as the players' avatar is always rendered on top of their head.
        /// </summary>
        public override void OnStartLocalPlayer()
        {
            GetComponentInChildren<MeshRenderer>().material.color = Color.blue;
        }

        /// <summary>
        /// Called on the host when a bullet needs to be added. 
        /// This will 'spawn' the bullet on all clients, including the 
        /// client on the host.
        /// </summary>
        [Command]
        void CmdFire()
        {
            Vector3 bulletDir = transform.forward;
            Vector3 bulletPos = transform.position + bulletDir * 1.5f;

            // The bullet needs to be transformed relative to the shared anchor.
            GameObject nextBullet = (GameObject)Instantiate(bullet, sharedWorldAnchorTransform.InverseTransformPoint(bulletPos), Quaternion.Euler(bulletDir));
            nextBullet.GetComponentInChildren<Rigidbody>().velocity = bulletDir * 1.0f;
            NetworkServer.Spawn(nextBullet);

            // Clean up the bullet in 8 seconds.
            Destroy(nextBullet, 8.0f);
        }

        public void OnInputClicked(InputClickedEventData eventData)
        {
            if (isLocalPlayer)
            {
                CmdFire();
            }
        }

        [Command]
        private void CmdSendSharedTransform(GameObject target, Vector3 pos, Quaternion rot)
        {
            UNetSharedHologram ush = target.GetComponent<UNetSharedHologram>();
            ush.CmdTransform(pos, rot);
        }

        /// <summary>
        /// For sending transforms for holograms which do not frequently change.
        /// </summary>
        /// <param name="target">The shared hologram</param>
        /// <param name="pos">position relative to the shared anchor</param>
        /// <param name="rot">rotation relative to the shared anchor</param>
        public void SendSharedTransform(GameObject target, Vector3 pos, Quaternion rot)
        {
            if (isLocalPlayer)
            {
                CmdSendSharedTransform(target, pos, rot);
            }
        }
    }
}
