using UnityEngine;
using System.Collections.Generic;
using System;
using FishNet.Object.Helping;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor.Experimental.SceneManagement;
using UnityEditor.SceneManagement;
using UnityEditor;
#endif

namespace FishNet.Object
{
    public sealed partial class NetworkObject : MonoBehaviour
    {
        #region Serialized.

        /// <summary>
        /// Networked PrefabId assigned to this Prefab.
        /// </summary>
        [field: SerializeField, HideInInspector]
        public ushort PrefabId { get; internal set; } = 0;
        /// <summary>
        /// Spawn collection to use assigned to this Prefab.
        /// </summary>
        [field: SerializeField, HideInInspector]
        public ushort SpawnableCollectionId { get; internal set; } = 0;
#pragma warning disable 414 //Disabled because Unity thinks tihs is unused when building.
        /// <summary>
        /// Hash to the scene which this object resides.
        /// </summary>
        [SerializeField, HideInInspector]
        private uint _scenePathHash;
#pragma warning restore 414
        /// <summary>
        /// Network Id for this scene object.
        /// </summary>
        [field: SerializeField, HideInInspector]
        internal ulong SceneId { get; private set; }
        /// <summary>
        /// Hash for the path which this asset resides. This value is set during edit time.
        /// </summary> 
        [field: SerializeField, HideInInspector]
        public ulong AssetPathHash { get; private set; }
        /// <summary>
        /// Sets AssetPathhash value.
        /// </summary>
        /// <param name="value">Value to use.</param>
        public void SetAssetPathHash(ulong value) => AssetPathHash = value;
        #endregion

#if UNITY_EDITOR
        /// <summary>
        /// This is used to store NetworkObjects in the scene during edit time.
        /// SceneIds are compared against this collection to ensure there are no duplicated.
        /// </summary>
        [SerializeField, HideInInspector]
        private List<NetworkObject> _sceneNetworkObjects = new List<NetworkObject>();
#endif

        /// <summary>
        /// Removes SceneObject state.
        /// This may only be called at runtime.
        /// </summary>
        internal void ClearRuntimeSceneObject()
        {
            if (!Application.isPlaying)
            {
                Debug.LogError($"ClearRuntimeSceneObject may only be called at runtime.");
                return;
            }

            SceneId = 0;
        }

#if UNITY_EDITOR
        /// <summary>
        /// Tries to generate a SceneId.
        /// </summary>
        internal void TryCreateSceneID()
        {
            if (Application.isPlaying)
                return;
            //Unity bug, sometimes this can be null depending on editor callback orders.
            if (gameObject == null)
                return;
            //Not a scene object.
            if (string.IsNullOrEmpty(gameObject.scene.name))
            {
                SceneId = 0;
                return;
            }

            ulong startId = SceneId;
            uint startPath = _scenePathHash;

            ulong sceneId = 0;
            uint scenePathHash = 0;
            //If prefab or part of a prefab, not a scene object.            
            if (PrefabUtility.IsPartOfPrefabAsset(this) || IsEditingInPrefabMode() ||
             //Not in a scene, another prefab check.
             !gameObject.scene.IsValid() ||
             //Stored on disk, so is a prefab. Somehow prefabutility missed it.
             EditorUtility.IsPersistent(this))
            {
                //These are all failing conditions, don't do additional checks.
            }
            else
            {
                System.Random rnd = new System.Random();
                scenePathHash = gameObject.scene.path.ToLower().GetStableHash32();
                sceneId = SceneId;
                //Not a valid sceneId or is a duplicate. 
                if (scenePathHash != _scenePathHash || SceneId == 0 || IsDuplicateSceneId(SceneId))
                {
                    /* If a scene has not been opened since an id has been
                     * generated then it will not be serialized in editor. The id
                     * would be correct in build but not if running in editor. 
                     * Should conditions be true where scene is building without
                     * being opened then cancel build and request user to open and save
                     * scene. */
                    if (BuildPipeline.isBuildingPlayer)
                        throw new InvalidOperationException($"Networked GameObject {gameObject.name} in scene {gameObject.scene.path} is missing a SceneId. Open the scene, select the Fish-Networking menu, and choose Rebuild SceneIds. If the problem persist ensures {gameObject.name} does not have any missing script references on it's prefab or in the scene. Also ensure that you have any prefab changes for the object applied.");

                    ulong shiftedHash = (ulong)scenePathHash << 32;
                    ulong randomId = 0;
                    while (randomId == 0 || IsDuplicateSceneId(randomId))
                    {
                        uint next = (uint)(rnd.Next(int.MinValue, int.MaxValue) + int.MaxValue);
                        /* Since the collection is lost when a scene loads the it's possible to
                        * have a sceneid from another scene. Because of this the scene path is
                        * inserted into the sceneid. */
                        randomId = (next & 0xFFFFFFFF) | shiftedHash;
                    }

                    sceneId = randomId;
                }

            }

            bool idChanged = (sceneId != startId);
            bool pathChanged = (startPath != scenePathHash);
            //If either changed then dirty and set.
            if (idChanged || pathChanged)
            {
                //Set dirty so changes will be saved.
                EditorUtility.SetDirty(this);
                /* Add to sceneIds collection. This must be done
                 * even if a new sceneId was not generated because
                 * the collection information is lost when the
                 * scene is existed. Essentially, it gets repopulated
                 * when the scene is re-opened. */
                SceneId = sceneId;
                _scenePathHash = scenePathHash;
            }
        }

        private bool IsEditingInPrefabMode()
        {
            if (EditorUtility.IsPersistent(this))
            {
                // if the game object is stored on disk, it is a prefab of some kind, despite not returning true for IsPartOfPrefabAsset =/
                return true;
            }
            else
            {
                // If the GameObject is not persistent let's determine which stage we are in first because getting Prefab info depends on it
                StageHandle mainStage = StageUtility.GetMainStageHandle();
                StageHandle currentStage = StageUtility.GetStageHandle(gameObject);
                if (currentStage != mainStage)
                {
                    var prefabStage = PrefabStageUtility.GetPrefabStage(gameObject);
                    if (prefabStage != null)
                    {
                        return true;
                    }
                }
            }
            return false;

        }

        /// <summary>
        /// Returns if the Id used is a sceneId already belonging to another object.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        private bool IsDuplicateSceneId(ulong id)
        {
            //Find all nobs in scene.
            _sceneNetworkObjects = GameObject.FindObjectsOfType<NetworkObject>().ToList();
            foreach (NetworkObject nob in _sceneNetworkObjects)
            {
                if (nob != null && nob != this && nob.SceneId == id)
                    return true;
            }
            //If here all checks pass.
            return false;
        }

        private void ReferenceIds_OnValidate()
        {
            TryCreateSceneID();
        }
        private void ReferenceIds_Reset()
        {
            TryCreateSceneID();
        }
#endif
    }

}