﻿using System;
using System.Collections.Generic;
using UnityEngine;
using VRC.SDK3.Components;
using VRC.SDKBase;

namespace VRC.SDK3.ClientSim
{
    /// <summary>
    /// System that keeps track of all synced objects. Handles changing ownership when players leave
    /// and also checks for if position synced objects fall below the respawn height.
    /// </summary>
    /// <remarks>
    /// Listens to Events:
    /// - ClientSimOnPlayerLeftEvent
    /// </remarks>
    [AddComponentMenu("")]
    public class ClientSimSyncedObjectManager : ClientSimBehaviour, IClientSimSyncedObjectManager, IDisposable
    {
        // Collection of all synced objects initialized in the scene. 
        private readonly ClientSimObjectCollection<IClientSimSyncable> _syncedObjects =
            new ClientSimObjectCollection<IClientSimSyncable>();
        
        // Collection of all position synced objects initialized in the scene. 
        private readonly ClientSimObjectCollection<IClientSimPositionSyncable> _positionSyncedObjects =
            new ClientSimObjectCollection<IClientSimPositionSyncable>();
        
        // Collection of all synced playerObjects initialized in the scene.
        private readonly ClientSimObjectCollection<IClientSimSyncable> _playerObjects =
            new ClientSimObjectCollection<IClientSimSyncable>();

        // TODO add generic system for saving synced data and restoring it to simulate late joining into a world.
        // TODO add system to manage udon synced data
        
        private IClientSimEventDispatcher _eventDispatcher;
        private IClientSimSceneManager _sceneManager;
        private IClientSimPlayerManager _playerManager;

        public void Initialize(
            IClientSimEventDispatcher eventDispatcher, 
            IClientSimSceneManager sceneManager, 
            IClientSimPlayerManager playerManager)
        {
            _eventDispatcher = eventDispatcher;
            _sceneManager = sceneManager;
            _playerManager = playerManager;
            
            _eventDispatcher.Subscribe<ClientSimOnPlayerLeftEvent>(OnPlayerLeft);
        }

        private void OnDestroy()
        {
            Dispose();
        }

        public void Dispose()
        {
            _eventDispatcher?.Unsubscribe<ClientSimOnPlayerLeftEvent>(OnPlayerLeft);
        }

        public void InitializeObjectSync(VRCObjectSync sync)
        {
            // Only allow one object sync helper per object.
            if (sync.TryGetComponent(out ClientSimObjectSyncHelper syncHelper))
            {
                syncHelper.Initialize(sync,this);
            }
            else
            {
                ClientSimObjectSyncHelper helper = sync.gameObject.AddComponent<ClientSimObjectSyncHelper>();
                helper.Initialize(sync,this);
            }
        }
        
        public void InitializeObjectPool(VRCObjectPool objectPool)
        {
            objectPool.gameObject.AddComponent<ClientSimObjectPoolHelper>().Initialize(objectPool, this);
        }

        private void LateUpdate()
        {
            ProcessPositionSyncedObjects();
        }

        private void ProcessPositionSyncedObjects()
        {
            _positionSyncedObjects.ProcessAddedAndRemovedObjects();
            
            // TODO space this out so that there are only x number per frame instead of all every time
            List<GameObject> objsToDestroy = new List<GameObject>();
            foreach (IClientSimPositionSyncable sync in _positionSyncedObjects.GetObjects())
            {
                if (sync == null)
                {
                    _positionSyncedObjects.ShouldVerifyObjects();
                    continue;
                }

                if (!sync.SyncPosition)
                {
                    continue;
                }

                // VRChatBug: The following method will enforce users to use VRCObjectSync's methods for setting
                // useGravity and isKinematic, but the current SDK does not support the hook to allow this.
                // Verify Sync properties, eg check if useGravity and isKinematic is properly set. 
                // sync.UpdatePositionSync();
                
                // Verify if the object is below respawn.
                Transform syncTransform = sync.GetTransform();
                if (syncTransform.position.y < _sceneManager.GetRespawnHeight())
                {
                    if (_sceneManager.ShouldObjectsDestroyAtRespawnHeight())
                    {
                        objsToDestroy.Add(syncTransform.gameObject);
                    }
                    else
                    {
                        sync.Respawn();
                    }
                }
            }

            foreach (var obj in objsToDestroy)
            {
                Destroy(obj);
            }
        }

        #region ClientSim Events
        
        // Handle updating object ownership for all objects the leaving player previously owned.
        private void OnPlayerLeft(ClientSimOnPlayerLeftEvent leftEvent)
        {
            VRCPlayerApi leftPlayer = leftEvent.player;
            int leftPlayerId = leftPlayer.playerId;
            VRCPlayerApi masterPlayer = _playerManager.GetMaster();
            if (masterPlayer == null)
            {
                return;
            }
            
            _syncedObjects.ProcessAddedAndRemovedObjects();
            foreach (IClientSimSyncable sync in _syncedObjects.GetObjects())
            {
                if (sync == null)
                {
                    continue;
                }

                if (sync is Component syncComp)
                {
                    if(syncComp == null) continue;
                    
                    GameObject syncObj = syncComp.gameObject;
                    if (Networking.GetOwner(syncObj)?.playerId == leftPlayerId)
                    {
                        Networking.SetOwner(masterPlayer, syncObj);
                    }
                }
                else
                {
                    if (sync.GetOwner() == leftPlayerId)
                    {
                        sync.SetOwner(masterPlayer.playerId);
                    }
                }
            }
        }

        #endregion

        #region IClientSimSyncedObjectManager

        public void AddSyncedObject(IClientSimSyncable sync)
        {
            _syncedObjects.AddObject(sync);
            _syncedObjects.ProcessAddedAndRemovedObjects();
            
            if (sync is IClientSimPositionSyncable posSync && posSync.SyncPosition)
            {
                _positionSyncedObjects.AddObject(posSync);
                _positionSyncedObjects.ProcessAddedAndRemovedObjects();
            }
            
            if((sync as MonoBehaviour).GetComponentInParent<VRCPlayerObject>() != null)
            {
                _playerObjects.AddObject(sync);
                _playerObjects.ProcessAddedAndRemovedObjects();
            }
        }

        public void RemoveSyncedObject(IClientSimSyncable sync)
        {
            _syncedObjects.RemoveObject(sync);
            _syncedObjects.ProcessAddedAndRemovedObjects();
            
            if (sync is IClientSimPositionSyncable posSync && posSync.SyncPosition)
            {
                _positionSyncedObjects.RemoveObject(posSync);
                _positionSyncedObjects.ProcessAddedAndRemovedObjects();
            }
            
            if((sync as MonoBehaviour).GetComponentInParent<VRCPlayerObject>() != null)
            {
                _playerObjects.RemoveObject(sync);
                _playerObjects.ProcessAddedAndRemovedObjects();
            }
        }

        #endregion
    }
}