using System.Collections;
using ABI_RC.Core.IO;
using ABI_RC.Core.Player;
using ABI_RC.Core.Player.AvatarTracking;
using ABI_RC.Core.UI;
using ABI_RC.Systems.GameEventSystem;
using NAK.AvatarScaleMod.Components;
using NAK.AvatarScaleMod.Networking;
using UnityEngine;

namespace NAK.AvatarScaleMod.AvatarScaling;

public class AvatarScaleManager : MonoBehaviour
{
    public static AvatarScaleManager Instance { get; private set; }
    
    private LocalScaler _localAvatarScaler;
    private Dictionary<string, NetworkScaler> _networkedScalers;
    
    private Coroutine _heightUpdateCoroutine;
    private readonly YieldInstruction _heightUpdateYield = new WaitForEndOfFrame();
    
    #region Universal Scaling Limits

    // ReSharper disable MemberCanBePrivate.Global
    // To match AvatarScaleTool: https://github.com/NotAKidoS/AvatarScaleTool/tree/main
    public const float DefaultMinHeight = 0.25f;
    public const float DefaultMaxHeight = 2.50f;
    // ReSharper restore MemberCanBePrivate.Global
    
    // Universal Scaling Limits
    public static float MinHeight { get; private set; } = DefaultMinHeight;
    public static float MaxHeight { get; private set; } = DefaultMaxHeight;

    #endregion
    
    #region Settings

    private bool _settingUniversalScaling;
    public bool Setting_UniversalScaling
    {
        get => _settingUniversalScaling;
        set
        {
            if (_settingUniversalScaling == value)
                return;
            
            _settingUniversalScaling = value;
            
            if (_localAvatarScaler != null)
                _localAvatarScaler.UseTargetHeight = value;
        }
    }

    private bool _settingAnimationClipScalingOverride;
    public bool Setting_AnimationClipScalingOverride
    {
        get => _settingAnimationClipScalingOverride;
        set
        {
            if (_settingAnimationClipScalingOverride == value)
                return;
            
            _settingAnimationClipScalingOverride = value;
            
            if (_localAvatarScaler != null)
                _localAvatarScaler.overrideAnimationHeight = value;
        }
    }

    private bool _settingPersistentHeight;
    public bool Setting_PersistentHeight
    {
        get => _settingPersistentHeight;
        set
        {
            if (_settingPersistentHeight == value)
                return;
            
            _settingPersistentHeight = value;
            
            // if (_localAvatarScaler != null)
            //     _localAvatarScaler.persistHeight = value;
        }
    }
    
    private float _lastTargetHeight = -1f;
    public float LastTargetHeight
    {
        get => _lastTargetHeight;
        set
        {
            _lastTargetHeight = value;
            ModSettings.EntryHiddenAvatarHeight.Value = _lastTargetHeight;
        }
    }

    #endregion
    
    #region Unity Events

    private void Awake()
    {
        if (Instance != null
            && Instance != this)
        {
            DestroyImmediate(this);
            return;
        }

        Instance = this;
        _networkedScalers = new Dictionary<string, NetworkScaler>();
    }

    private void Start()
    {
        _localAvatarScaler = PlayerSetup.Instance.gameObject.AddComponent<LocalScaler>();
        _localAvatarScaler.Initialize();
        
        _settingUniversalScaling = ModSettings.EntryUseUniversalScaling.Value;
        Setting_AnimationClipScalingOverride = ModSettings.EntryAnimationScalingOverride.Value;
        Setting_PersistentHeight = ModSettings.EntryPersistentHeight.Value;
        _lastTargetHeight = ModSettings.EntryPersistThroughRestart.Value 
            ? ModSettings.EntryHiddenAvatarHeight.Value : -1f; // -1f is default
        
        // listen for events
        _localAvatarScaler.OnAnimatedHeightOverride += OnAnimationHeightOverride;
        
        CVRGameEventSystem.Instance.OnConnected.AddListener(OnInstanceConnected);
    }
    
    private void OnEnable()
    {
        if (_heightUpdateCoroutine != null) StopCoroutine(_heightUpdateCoroutine);
        _heightUpdateCoroutine = StartCoroutine(HeightUpdateCoroutine());
    }
    
    private void OnDisable()
    {
        if (_heightUpdateCoroutine != null) StopCoroutine(_heightUpdateCoroutine);
        _heightUpdateCoroutine = null;
    }

    private void OnDestroy()
    {
        _heightUpdateCoroutine = null;
        CVRGameEventSystem.Instance.OnConnected.RemoveListener(OnInstanceConnected);

        if (_localAvatarScaler != null) Destroy(_localAvatarScaler);
        _localAvatarScaler = null;
        
        foreach (NetworkScaler scaler in _networkedScalers.Values) Destroy(scaler);
        _networkedScalers.Clear();
        
        if (Instance == this)
            Instance = null;
    }
    
    // only update the height per scaler once per frame, to prevent spam & jitter
    // this is to ensure that the height is also set at correct time during frame, no matter when it is called
    private IEnumerator HeightUpdateCoroutine()
    {
        while (enabled) yield return _heightUpdateYield;
        // ReSharper disable once IteratorNeverReturns
    }

    #endregion

    #region Game Events

    public void OnInstanceConnected(string instanceId)
    {
        // TODO: need to know if this causes issues when in a reconnection loop
        SchedulerSystem.AddJob(ModNetwork.RequestHeightSync, 2f, 1f, 1);
    }
    
    #endregion

    #region Local Methods

    public void OnAvatarInstantiated(PlayerSetup playerSetup)
    {
        if (playerSetup._avatar == null)
            return;

        _localAvatarScaler.OnAvatarInstantiated(playerSetup._avatar, playerSetup._initialAvatarHeight,
            playerSetup.initialScale);
        
        if (!_settingUniversalScaling)
            return;

        SetTargetHeight(_lastTargetHeight);
    }

    public void OnAvatarDestroyed(PlayerSetup playerSetup)
    {
        if (_localAvatarScaler != null)
            _localAvatarScaler.OnAvatarDestroyed();
    }

    public void SetTargetHeight(float targetHeight)
    {
        _lastTargetHeight = targetHeight; // save for persistent height
        
        if (_localAvatarScaler == null)
            return;
        
        _localAvatarScaler.TargetHeight = _lastTargetHeight;
    }

    public float GetHeight()
    {
        if (_localAvatarScaler == null)
            return PlayerAvatarPoint.defaultAvatarHeight;

        if (!_localAvatarScaler.IsForcingHeight())
            return PlayerSetup.Instance.GetAvatarHeight();
        
        return _localAvatarScaler.GetTargetHeight();
    }
    
    public float GetAnimationClipHeight()
    {
        if (_localAvatarScaler == null)
            return PlayerAvatarPoint.defaultAvatarHeight;

        if (!_localAvatarScaler.IsForcingHeight())
            return PlayerSetup.Instance.GetAvatarHeight();
        
        return _localAvatarScaler.GetAnimatedHeight();
    }
    
    public float GetHeightForNetwork()
    {
        if (!_settingUniversalScaling)
            return -1f;
        
        if (_localAvatarScaler == null)
            return -1f;
        
        if (!_localAvatarScaler.IsForcingHeight())
            return -1f;

        return _localAvatarScaler.GetTargetHeight();
    }

    public float GetInitialHeight()
    {
        if (_localAvatarScaler == null)
            return -1f;

        return _localAvatarScaler.GetInitialHeight();
    }

    public bool IsHeightAdjustedFromInitial()
    {
        return _localAvatarScaler != null && _localAvatarScaler.IsForcingHeight();
    }

    #endregion

    #region Network Methods

    public bool DoesNetworkHeightScalerExist(string playerId)
        => _networkedScalers.ContainsKey(playerId);
    
    public int GetNetworkHeightScalerCount()
        => _networkedScalers.Count;
    
    public float GetNetworkHeight(string playerId)
    {
        if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler))
            if (scaler.IsForcingHeight()) return scaler.GetTargetHeight();
        
        //doesn't have mod or has no custom height, get from player avatar directly
        CVRPlayerEntity playerEntity = CVRPlayerManager.Instance.NetworkPlayers.Find((players) => players.Uuid == playerId);
        if (playerEntity != null && playerEntity.PuppetMaster != null)
            return playerEntity.PuppetMaster.netIkController.GetRemoteHeight();
        
        // player is invalid???
        return -1f;
    }

    // we will create a Universal Scaler for only users that send a height update
    // this is sent at a rate of 10s locally!

    internal void OnNetworkHeightUpdateReceived(string playerId, float targetHeight)
    {
        if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler))
            scaler.TargetHeight = targetHeight;
        else
            SetupHeightScalerForNetwork(playerId, targetHeight);
    }

    internal void OnNetworkAvatarInstantiated(PuppetMaster puppetMaster)
    {
        var playerId = puppetMaster._playerDescriptor.ownerId;
        if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler))
            scaler.OnAvatarInstantiated(puppetMaster.avatarObject, puppetMaster.netIkController._initialHeight,
                puppetMaster.netIkController._initialScale);
    }

    internal void OnNetworkAvatarDestroyed(PuppetMaster puppetMaster)
    {
        // on disconnect
        if (puppetMaster == null || puppetMaster._playerDescriptor == null)
            return;

        var playerId = puppetMaster._playerDescriptor.ownerId;
        if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler))
            scaler.OnAvatarDestroyed();
    }

    internal void RemoveNetworkHeightScaler(string playerId)
    {
        if (_networkedScalers.ContainsKey(playerId))
        {
            AvatarScaleMod.Logger.Msg(
                $"Removed user height scaler! This is hopefully due to a disconnect or block. : {playerId}");

            _networkedScalers.Remove(playerId);
            return;
        }

        AvatarScaleMod.Logger.Msg(
            $"Failed to remove a user height scaler! This shouldn't happen. : {playerId}");
    }

    private void SetupHeightScalerForNetwork(string playerId, float targetHeight)
    {
        CVRPlayerEntity playerEntity =
            CVRPlayerManager.Instance.NetworkPlayers.Find(players => players.Uuid == playerId);

        PuppetMaster puppetMaster = playerEntity?.PuppetMaster;

        if (playerEntity == null || puppetMaster == null)
        {
            AvatarScaleMod.Logger.Error(
                $"Attempted to set up height scaler for user which does not exist! : {playerId}");
            return;
        }

        AvatarScaleMod.Logger.Msg(
            $"Setting up new height scaler for user which has sent a height update! : {playerId}");

        if (_networkedScalers.ContainsKey(playerId))
            _networkedScalers.Remove(playerId); // ??

        NetworkScaler scaler = puppetMaster.gameObject.AddComponent<NetworkScaler>();
        scaler.Initialize(playerId);

        scaler.OnAvatarInstantiated(puppetMaster.avatarObject, puppetMaster.netIkController._initialHeight,
            puppetMaster.netIkController._initialScale);

        _networkedScalers[playerId] = scaler;

        scaler.TargetHeight = targetHeight;
    }

    #endregion

    #region Manager Methods
    
    // sometimes fun to play with via UE
    public void SetUniversalScalingLimit(float min, float max)
    {
        const float HardCodedMinLimit = 0.01f;
        const float HardCodedMaxLimit = 100f;
        
        MinHeight = Mathf.Clamp(min, HardCodedMinLimit, HardCodedMaxLimit);
        MaxHeight = Mathf.Clamp(max, HardCodedMinLimit, HardCodedMaxLimit);
        
        AvatarScaleMod.Logger.Msg($"Universal Scaling Limits changed: {min} - {max}");
        AvatarScaleMod.Logger.Warning("This will not network to other users unless they also have the same limits set!");
    }
    
    public void ResetUniversalScalingLimit()
    {
        MinHeight = DefaultMinHeight;
        MaxHeight = DefaultMaxHeight;
        
        AvatarScaleMod.Logger.Msg("Universal Scaling Limits reset to default!");
    }

    #endregion

    #region Event Listeners
    
    private static void OnAnimationHeightOverride(BaseScaler scaler)
    {
        AvatarScaleMod.Logger.Msg("AnimationClip-based avatar scaling detected. Disabling Universal Scaling.");
        CohtmlHud.Instance.ViewDropTextImmediate("(Local) AvatarScaleMod", "Avatar Scale Changed!",
            "Universal Scaling is now disabled in favor of built-in avatar scaling.");
    }

    #endregion
}