﻿using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Core.Libraries.Covalence;
using System;
using System.Collections.Generic;
using UnityEngine;
using VLB;

namespace Oxide.Plugins
{
    [Info("Drone Scale Manager", "WhiteThunder", "1.0.2")]
    [Description("Utilities for resizing RC drones.")]
    internal class DroneScaleManager : CovalencePlugin
    {
        #region Fields

        [PluginReference]
        Plugin DroneEffects, EntityScaleManager;

        private static DroneScaleManager _pluginInstance;
        private static StoredData _pluginData;

        private const string PermissionScaleUnrestricted = "dronescalemanager.unrestricted";

        private const string SpherePrefab = "assets/prefabs/visualization/sphere.prefab";

        private const float VanillaDroneGroundTraceDistance = 0.1f;
        private const float VanillaDroneYawSpeed = 2;
        private const float RootEntityLocalY = 0.2f;

        private static readonly Vector3 RootEntityPosition = new Vector3(0, RootEntityLocalY, 0);

        #endregion

        #region Hooks

        private void Init()
        {
            _pluginInstance = this;
            _pluginData = StoredData.Load();

            permission.RegisterPermission(PermissionScaleUnrestricted, this);
        }

        private void OnServerInitialized()
        {
            if (!VerifyDependencies())
                return;

            RefreshAllScaledDrones();
        }

        private void Unload()
        {
            _pluginData.Save();

            DroneCollisionProxy.DestroyAll();

            _pluginData = null;
            _pluginInstance = null;
        }

        private void OnServerSave()
        {
            _pluginData.Save();
        }

        private void OnNewSave()
        {
            _pluginData = StoredData.Clear();
        }

        private void OnPluginLoaded(Plugin plugin)
        {
            if (plugin == EntityScaleManager)
                RefreshAllScaledDrones();
        }

        private void OnEntityKill(Drone drone)
        {
            if (!_pluginData.ScaledDrones.Remove(drone.net.ID.Value))
                return;

            var rootEntity = GetRootEntity(drone);
            if (rootEntity == null)
                return;

            rootEntity.Invoke(() =>
            {
                if (!rootEntity.IsDestroyed)
                    rootEntity.Kill();
            }, 0);
        }

        // This hook is exposed by plugin: Drone Effects (DroneEffects).
        private bool? OnDroneAnimationStart(Drone drone)
        {
            // Prevent the animated delivery drone from spawning, since it may be normal size.
            if (IsScaledDrone(drone))
                return false;

            return null;
        }

        #endregion

        #region API

        private bool API_ScaleDrone(Drone drone, float scale)
        {
            return TryScaleDrone(drone, scale);
        }

        private bool API_ParentEntity(Drone drone, BaseEntity childEntity)
        {
            if (!IsDroneEligible(drone) || !IsScaledDrone(drone))
                return false;

            var rootEntity = GetRootEntityOrParentSphere(drone);
            if (rootEntity == null)
                return false;

            PositionChildTransform(rootEntity.transform, drone.transform, childEntity.transform);
            childEntity.SetParent(rootEntity, worldPositionStays: false, sendImmediate: true);

            if (!childEntity.isSpawned)
                childEntity.Spawn();

            return true;
        }

        private bool API_ParentTransform(Drone drone, Transform childTransform)
        {
            if (!IsDroneEligible(drone) || !IsScaledDrone(drone))
                return false;

            var rootEntity = GetRootEntityOrParentSphere(drone);
            if (rootEntity == null)
                return false;

            var rootTransform = rootEntity.transform;
            PositionChildTransform(rootTransform, drone.transform, childTransform);
            childTransform.parent = rootTransform;

            return true;
        }

        private Drone API_GetParentDrone(BaseEntity entity)
        {
            var possibleRootEntity = GetParentSphere(entity);
            if (possibleRootEntity == null)
                return null;

            var drone = GetGrandChildOfType<Drone>(possibleRootEntity);
            if (drone == null || !IsScaledDrone(drone))
                return null;

            return drone;
        }

        private SphereEntity API_GetRootEntity(Drone drone)
        {
            return GetRootEntity(drone);
        }

        #endregion

        #region Commands

        [Command("scaledrone", "dronescale")]
        private void CommandDroneScale(IPlayer player, string cmd, string[] args)
        {
            if (player.IsServer)
                return;

            if (!player.HasPermission(PermissionScaleUnrestricted))
            {
                ReplyToPlayer(player, Lang.ErrorNoPermission);
                return;
            }

            float scale;
            if (args.Length == 0 || !float.TryParse(args[0], out scale))
            {
                ReplyToPlayer(player, Lang.ErrorSyntax, cmd);
                return;
            }

            var basePlayer = player.Object as BasePlayer;
            var drone = GetLookEntity(basePlayer) as Drone;
            if (drone == null || !IsDroneEligible(drone))
            {
                ReplyToPlayer(player, Lang.ErrorNoDroneFound);
                return;
            }

            if (TryScaleDrone(drone, scale))
                ReplyToPlayer(player, Lang.ScaleSuccess, scale);
            else
                ReplyToPlayer(player, Lang.ScaleError);
        }

        #endregion

        #region Helper Methods

        private static bool DroneScaleWasBlocked(Drone drone, float scale)
        {
            object hookResult = Interface.CallHook("OnDroneScale", drone, scale);
            return hookResult is bool && (bool)hookResult == false;
        }

        private static bool VerifyDependencies()
        {
            if (_pluginInstance.EntityScaleManager == null)
            {
                _pluginInstance.LogError("EntityScaleManager is not loaded, get it at https://umod.org");
                return false;
            }

            return true;
        }

        private static bool IsScaledDrone(Drone drone) =>
            _pluginData.ScaledDrones.Contains(drone.net.ID.Value);

        private static float GetDroneScale(Drone drone)
        {
            if (!VerifyDependencies())
                return 1;

            return Convert.ToSingle(_pluginInstance.EntityScaleManager.Call("API_GetScale", drone));
        }

        private static bool IsDroneEligible(Drone drone) => !(drone is DeliveryDrone);

        private static BaseEntity GetLookEntity(BasePlayer basePlayer, float maxDistance = 20)
        {
            RaycastHit hit;
            return Physics.Raycast(basePlayer.eyes.HeadRay(), out hit, maxDistance, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore)
                ? hit.GetEntity()
                : null;
        }

        private static T GetGrandChildOfType<T>(BaseEntity entity) where T : BaseEntity
        {
            foreach (var child in entity.children)
            {
                foreach (var grandChild in child.children)
                {
                    var grandChildOfType = grandChild as T;
                    if (grandChildOfType != null)
                        return grandChildOfType;
                }
            }
            return null;
        }

        private static void CopyRigidBodySettings(Rigidbody sourceBody, Rigidbody destinationBody)
        {
            // Copy everything except rotation since we are handling that at the transform level instead.
            destinationBody.angularDrag = sourceBody.angularDrag;
            destinationBody.centerOfMass = sourceBody.centerOfMass;
            destinationBody.collisionDetectionMode = sourceBody.collisionDetectionMode;
            destinationBody.constraints = sourceBody.constraints;
            destinationBody.detectCollisions = sourceBody.detectCollisions;
            destinationBody.drag = sourceBody.drag;
            destinationBody.freezeRotation = sourceBody.freezeRotation;
            destinationBody.inertiaTensorRotation = sourceBody.inertiaTensorRotation;
            destinationBody.interpolation = sourceBody.interpolation;
            destinationBody.isKinematic = sourceBody.isKinematic;
            destinationBody.mass = sourceBody.mass;
            destinationBody.maxAngularVelocity = sourceBody.maxAngularVelocity;
            destinationBody.maxDepenetrationVelocity = sourceBody.maxDepenetrationVelocity;
            destinationBody.sleepThreshold = sourceBody.sleepThreshold;
            destinationBody.solverIterations = sourceBody.solverIterations;
            destinationBody.solverVelocityIterations = sourceBody.solverVelocityIterations;
            destinationBody.useGravity = sourceBody.useGravity;
        }

        private static void MaybeSetupRootRigidBody(Drone scaledDrone, SphereEntity rootEntity)
        {
            var rootRigidBody = rootEntity.GetOrAddComponent<Rigidbody>();
            if (rootRigidBody == scaledDrone.body)
                return;

            CopyRigidBodySettings(scaledDrone.body, rootRigidBody);
            UnityEngine.Object.DestroyImmediate(scaledDrone.body);
            scaledDrone.body = rootRigidBody;
        }

        private static void RestoreRigidBody(Drone scaledDrone, SphereEntity rootEntity)
        {
            var rootRigidBody = rootEntity.GetComponent<Rigidbody>();
            if (rootRigidBody == null)
                return;

            scaledDrone.body = scaledDrone.GetOrAddComponent<Rigidbody>();
            CopyRigidBodySettings(rootRigidBody, scaledDrone.body);
        }

        private static void EnableGlobalBroadcastFixed(BaseEntity entity, bool wants)
        {
            entity.globalBroadcast = wants;

            if (wants)
            {
                entity.UpdateNetworkGroup();
            }
            else if (entity.net?.group?.ID == 0)
            {
                // Fix vanilla bug that prevents leaving the global network group.
                var group = entity.net.sv.visibility.GetGroup(entity.transform.position, entity.networkRange);
                entity.net.SwitchGroup(group);
            }
        }

        private static void SetupRootEntity(Drone drone, SphereEntity rootEntity)
        {
            rootEntity.gameObject.layer = drone.gameObject.layer;

            // SphereEntity has enableSaving off by default.
            // This fixes an issue where the resized child gets orphaned on restart and spams console errors every 2 seconds.
            rootEntity.EnableSaving(true);

            // SphereEntity has globalBroadcast on by default.
            // This fixes an issue where clients who resubscribe do not recreate the sphere or its children.
            EnableGlobalBroadcastFixed(rootEntity, false);

            // Move rigid body to the root entity.
            MaybeSetupRootRigidBody(drone, rootEntity);

            // Proxy collisions from the root entity to the drone.
            rootEntity.GetOrAddComponent<DroneCollisionProxy>().OwnerDrone = drone;
        }

        private static void SetupRootEntityAfterSpawn(Drone drone, SphereEntity rootEntity)
        {
            // Cancel the default position network updates since we will use fixed time updates instead.
            rootEntity.CancelInvoke(rootEntity.NetworkPositionTick);

            if (!rootEntity.IsInvokingFixedTime(rootEntity.NetworkPositionTick))
                rootEntity.InvokeRepeatingFixedTime(rootEntity.NetworkPositionTick);
        }

        private static void SetupScaledDrone(Drone drone, float scale)
        {
            // Without changing yaw speed, large drones turn faster and small drones turn slower.
            drone.yawSpeed = VanillaDroneYawSpeed / scale;
            drone.groundTraceDist = Math.Min(RootEntityLocalY, VanillaDroneGroundTraceDistance * scale + RootEntityLocalY);

            // Cancel position network updates since the root entity will be moved instead.
            if (scale != 1)
                drone.CancelInvokeFixedTime(drone.NetworkPositionTick);
        }

        private static void RefreshScaledDrone(Drone drone)
        {
            SphereEntity parentSphere;
            var rootEntity = GetRootEntity(drone, out parentSphere);
            if (rootEntity == null)
                return;

            SetupRootEntity(drone, rootEntity);
            SetupRootEntityAfterSpawn(drone, rootEntity);
            SetupScaledDrone(drone, parentSphere.currentRadius);
        }

        private void RefreshAllScaledDrones()
        {
            foreach (var entity in BaseNetworkable.serverEntities)
            {
                var drone = entity as Drone;
                if (drone == null || !IsDroneEligible(drone))
                    continue;

                if (IsScaledDrone(drone))
                    RefreshScaledDrone(drone);
            }
        }

        private static void PositionChildTransform(Transform rootTransform, Transform droneTransform, Transform childTransform)
        {
            var scaledWorldPosition = droneTransform.TransformPoint(childTransform.localPosition);
            childTransform.localPosition = rootTransform.InverseTransformPoint(scaledWorldPosition);
            childTransform.localRotation = droneTransform.localRotation * childTransform.localRotation;
        }

        private static SphereEntity GetParentSphere(BaseEntity entity) =>
            entity.GetParentEntity() as SphereEntity;

        private static SphereEntity GetRootEntity(Drone drone, out SphereEntity parentSphere)
        {
            parentSphere = GetParentSphere(drone);
            if (parentSphere == null)
                return null;

            return parentSphere.GetParentEntity() as SphereEntity;
        }

        private static SphereEntity GetRootEntity(Drone drone)
        {
            SphereEntity parentSphere;
            return GetRootEntity(drone, out parentSphere);
        }

        private static SphereEntity GetRootEntityOrParentSphere(Drone drone)
        {
            SphereEntity parentSphere;
            var rootEntity = GetRootEntity(drone, out parentSphere);
            if (rootEntity != null)
                return rootEntity;

            // Assume the parent sphere is the root entity, in case this was called during the OnDroneScaleBegin hook.
            if (parentSphere != null)
                return parentSphere;

            return null;
        }

        private static SphereEntity AddRootEntity(Drone drone)
        {
            var rootEntity = GameManager.server.CreateEntity(SpherePrefab, drone.transform.TransformPoint(RootEntityPosition)) as SphereEntity;
            if (rootEntity == null)
                return null;

            SetupRootEntity(drone, rootEntity);
            rootEntity.Spawn();
            SetupRootEntityAfterSpawn(drone, rootEntity);
            drone.SetParent(rootEntity, worldPositionStays: true, sendImmediate: true);

            return rootEntity;
        }

        private static void RemoveRootEntity(Drone scaledDrone)
        {
            SphereEntity parentSphere;
            var rootEntity = GetRootEntity(scaledDrone, out parentSphere);
            if (rootEntity == null)
                return;

            // Restore the movement updates since they were disabled while resized.
            if (!scaledDrone.IsInvokingFixedTime(scaledDrone.NetworkPositionTick))
                scaledDrone.InvokeRepeatingFixedTime(scaledDrone.NetworkPositionTick);

            parentSphere.SetParent(null, worldPositionStays: true, sendImmediate: true);

            RestoreRigidBody(scaledDrone, rootEntity);
            rootEntity.Kill();

            _pluginData.ScaledDrones.Remove(scaledDrone.net.ID.Value);
        }

        private static bool ScaleDrone(Drone drone, SphereEntity rootEntity, float scale, float currentScale)
        {
            SetupScaledDrone(drone, scale);

            // Notify other plugins before removing the root entity.
            // This allows plugins to move or remove attachments from the root entity if needed.
            Interface.CallHook("OnDroneScaleBegin", drone, rootEntity, scale, currentScale);

            if (scale == 1)
                RemoveRootEntity(drone);

            var result = _pluginInstance.EntityScaleManager.CallHook("API_ScaleEntity", drone, scale);
            var success = result is bool && (bool)result;

            // Reposition existing attachments if the drone is already resized.
            if (IsScaledDrone(drone))
            {
                var rootTransform = rootEntity.transform;
                var droneTransform = drone.transform;
                var droneParentSphere = GetParentSphere(drone);

                foreach (var child in rootEntity.children)
                {
                    if (child == droneParentSphere)
                        continue;

                    var childTransform = child.transform;
                    var baseLocalPosition = droneTransform.InverseTransformPoint(childTransform.position) / currentScale;
                    var newWorldPosition = droneTransform.TransformPoint(baseLocalPosition * scale);
                    childTransform.localPosition = rootTransform.InverseTransformPoint(newWorldPosition);

                    child.InvalidateNetworkCache();
                    child.SendNetworkUpdate_Position();
                }
            }

            return success;
        }

        private static bool TryScaleDrone(Drone drone, float desiredScale)
        {
            if (!VerifyDependencies())
                return false;

            if (DroneScaleWasBlocked(drone, desiredScale))
                return false;

            var currentScale = GetDroneScale(drone);
            var isCurrentlyScaled = currentScale != 1 && IsScaledDrone(drone);

            bool success = false;
            SphereEntity rootEntity;

            if (isCurrentlyScaled)
            {
                if (desiredScale == currentScale)
                    return true;

                rootEntity = GetRootEntity(drone);
                success = ScaleDrone(drone, rootEntity, desiredScale, currentScale);
            }
            else if (desiredScale == 1)
            {
                return true;
            }
            else
            {
                _pluginData.ScaledDrones.Add(drone.net.ID.Value);
                rootEntity = AddRootEntity(drone);
                success = ScaleDrone(drone, rootEntity, desiredScale, currentScale);
                _pluginInstance.DroneEffects?.Call("API_StopAnimating", drone);
            }

            Interface.CallHook("OnDroneScaled", drone, rootEntity, desiredScale, currentScale);
            return success;
        }

        #endregion

        #region Collision Detection

        private class DroneCollisionProxy : MonoBehaviour
        {
            private const string OnCollisionEnterMethodName = "OnCollisionEnter";
            private const string OnCollisionStayMethodName = "OnCollisionStay";

            public static void DestroyAll()
            {
                foreach (var entity in BaseNetworkable.serverEntities)
                {
                    var sphereEntity = entity as SphereEntity;
                    if (sphereEntity == null)
                        continue;

                    var component = sphereEntity.GetComponent<DroneCollisionProxy>();
                    if (component == null)
                        continue;

                    DestroyImmediate(component);
                }
            }

            public Drone OwnerDrone;

            private void OnCollisionEnter(Collision collision)
            {
                if (OwnerDrone != null)
                    OwnerDrone.BroadcastMessage(OnCollisionEnterMethodName, collision, SendMessageOptions.DontRequireReceiver);
            }

            private void OnCollisionStay()
            {
                if (OwnerDrone != null)
                    OwnerDrone.BroadcastMessage(OnCollisionStayMethodName, SendMessageOptions.DontRequireReceiver);
            }
        }

        #endregion

        #region Data

        private class StoredData
        {
            [JsonProperty("ScaledDrones")]
            public HashSet<ulong> ScaledDrones = new HashSet<ulong>();

            public static StoredData Load() =>
                Interface.Oxide.DataFileSystem.ReadObject<StoredData>(_pluginInstance.Name) ?? new StoredData();

            public static StoredData Clear() => new StoredData().Save();

            public StoredData Save()
            {
                Interface.Oxide.DataFileSystem.WriteObject(_pluginInstance.Name, this);
                return this;
            }
        }

        #endregion

        #region Localization

        private void ReplyToPlayer(IPlayer player, string messageName, params object[] args) =>
            player.Reply(string.Format(GetMessage(player, messageName), args));

        private string GetMessage(IPlayer player, string messageName, params object[] args) =>
            GetMessage(player.Id, messageName, args);

        private string GetMessage(string playerId, string messageName, params object[] args)
        {
            var message = lang.GetMessage(messageName, this, playerId);
            return args.Length > 0 ? string.Format(message, args) : message;
        }

        private class Lang
        {
            public const string ErrorNoPermission = "Error.NoPermission";
            public const string ErrorSyntax = "Error.Syntax";
            public const string ErrorNoDroneFound = "Error.NoDroneFound";
            public const string ScaleSuccess = "Scale.Success";
            public const string ScaleError = "Error.ScalePrevented";
        }

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                [Lang.ErrorNoPermission] = "You don't have permission to do that.",
                [Lang.ErrorSyntax] = "Syntax: {0} <size>",
                [Lang.ErrorNoDroneFound] = "Error: No drone found.",
                [Lang.ScaleSuccess] = "Drone was scaled to: {0}",
                [Lang.ScaleError] = "An error occurred while attempting to resize that drone.",
            }, this, "en");
        }

        #endregion
    }
}
