﻿using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Drone Lights", "WhiteThunder", "2.0.3")]
    [Description("Adds controllable search lights to RC drones.")]
    internal class DroneLights : CovalencePlugin
    {
        #region Fields

        private const string PermissionAutoDeploy = "dronelights.searchlight.autodeploy";
        private const string PermissionMoveLight = "dronelights.searchlight.move";

        private const string SearchLightPrefab = "assets/prefabs/deployable/search light/searchlight.deployed.prefab";

        private const float SearchLightYAxisRotation = 180;
        private const float SearchLightScale = 0.1f;

        private static readonly Vector3 SearchLightLocalPosition = new(0, -0.095f, 0.23f);
        private static readonly Vector3 SearchLightTransformScale = Vector3.one * SearchLightScale;
        private static readonly Vector3 SearchLightPivotPoint = new Vector3(0, -1.235f, 0) * SearchLightScale;

        private static readonly FieldInfo DronePitchField = typeof(Drone).GetField("pitch", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

        private Configuration _config;
        private ProtectionProperties _immortalProtection;

        #endregion

        #region Hooks

        private void Init()
        {
            permission.RegisterPermission(PermissionAutoDeploy, this);
            permission.RegisterPermission(PermissionMoveLight, this);
            Unsubscribe(nameof(OnEntitySpawned));
        }

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

                SearchLightUpdater.RemoveFromDrone(drone);
            }

            UnityEngine.Object.Destroy(_immortalProtection);
        }

        private void OnServerInitialized()
        {
            _immortalProtection = ScriptableObject.CreateInstance<ProtectionProperties>();
            _immortalProtection.name = "DroneLightsProtection";
            _immortalProtection.Add(1);

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

                AddOrUpdateSearchLight(drone);
            }

            foreach (var player in BasePlayer.activePlayerList)
            {
                var station = player.GetMounted() as ComputerStation;
                if (station == null)
                    continue;

                var drone = station.currentlyControllingEnt.Get(serverside: true) as Drone;
                if (drone == null)
                    continue;

                OnBookmarkControlStarted(station, player, string.Empty, drone);
            }

            Subscribe(nameof(OnEntitySpawned));
        }

        private void OnEntitySpawned(Drone drone)
        {
            if (!IsDroneEligible(drone))
                return;

            var drone2 = drone;
            NextTick(() =>
            {
                if (drone2 == null || drone2.IsDestroyed)
                    return;

                MaybeAutoDeploySearchLight(drone2);
            });
        }

        private void OnBookmarkControlStarted(ComputerStation station, BasePlayer player, string bookmarkName, Drone drone)
        {
            var controllerSteamId = drone.ControllingViewerId?.SteamId;
            if (controllerSteamId != player.userID)
                return;

            var searchLight = GetDroneSearchLight(drone);
            if (searchLight == null)
                return;

            var hasMovePermission = permission.UserHasPermission(player.UserIDString, PermissionMoveLight);
            if (!hasMovePermission)
            {
                var defaultAngle = _config.SearchLight.DefaultAngle - 90 % 360;
                SetLightAngle(drone, searchLight, searchLight.transform, defaultAngle);
            }

            SearchLightUpdater.AddOrUpdateForDrone(this, drone, searchLight, player, hasMovePermission);
        }

        #endregion

        #region Helper Methods

        private static bool DeployLightWasBlocked(Drone drone)
        {
            return Interface.CallHook("OnDroneSearchLightDeploy", drone) is false;
        }

        private static bool IsDroneEligible(Drone drone)
        {
            return drone.skinID == 0 && !(drone is DeliveryDrone);
        }

        private static Drone GetControlledDrone(ComputerStation station)
        {
            return station.currentlyControllingEnt.Get(serverside: true) as Drone;
        }

        private static Drone GetControlledDrone(BasePlayer player)
        {
            var computerStation = player.GetMounted() as ComputerStation;
            if (computerStation == null)
                return null;

            return GetControlledDrone(computerStation);
        }

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

            return null;
        }

        private static T2 GetGrandChildOfType<T1, T2>(BaseEntity entity, out T1 childOfType) where T1 : BaseEntity where T2 : BaseEntity
        {
            foreach (var child in entity.children)
            {
                childOfType = child as T1;
                if (childOfType == null)
                    continue;

                foreach (var grandChild in childOfType.children)
                {
                    var grandChildOfType = grandChild as T2;
                    if (grandChildOfType != null)
                        return grandChildOfType;
                }
            }

            childOfType = null;
            return null;
        }

        private static SearchLight GetDroneSearchLight(Drone drone, out SphereEntity parentSphere)
        {
            var searchLight = GetChildOfType<SearchLight>(drone);
            if (searchLight != null)
            {
                parentSphere = null;
                return searchLight;
            }

            return GetGrandChildOfType<SphereEntity, SearchLight>(drone, out parentSphere);
        }

        private static SearchLight GetDroneSearchLight(Drone drone)
        {
            return GetDroneSearchLight(drone, out _);
        }

        private static void RemoveProblemComponents(BaseEntity entity)
        {
            foreach (var collider in entity.GetComponentsInChildren<Collider>())
            {
                UnityEngine.Object.DestroyImmediate(collider);
            }

            UnityEngine.Object.DestroyImmediate(entity.GetComponent<DestroyOnGroundMissing>());
            UnityEngine.Object.DestroyImmediate(entity.GetComponent<GroundWatch>());
        }

        private static void HideInputsAndOutputs(IOEntity ioEntity)
        {
            // Trick to hide the inputs and outputs on the client.
            foreach (var input in ioEntity.inputs)
            {
                input.type = IOEntity.IOType.Generic;
            }

            foreach (var output in ioEntity.outputs)
            {
                output.type = IOEntity.IOType.Generic;
            }
        }

        private static void SetLightAngle(Drone drone, SearchLight searchLight, Transform transform, float overrideAngle = 0)
        {
            float desiredPitch;

            if (overrideAngle != 0)
            {
                desiredPitch = overrideAngle;
            }
            else
            {
                if (DronePitchField == null)
                    return;

                desiredPitch = (float)DronePitchField.GetValue(drone);
                desiredPitch = (360 - desiredPitch) % 360;
            }

            var currentPitch = transform.localEulerAngles.x;
            if (Math.Abs(currentPitch - desiredPitch) < 0.1f)
                return;

            var localRotation = Quaternion.Euler(desiredPitch, SearchLightYAxisRotation, 0);
            var localPosition = SearchLightLocalPosition + localRotation * SearchLightPivotPoint;
            transform.SetLocalPositionAndRotation(localPosition, localRotation);
            searchLight.InvalidateNetworkCache();

            // This is the most expensive line in terms of performance.
            searchLight.SendNetworkUpdate_Position();
        }

        private SearchLight TryDeploySearchLight(Drone drone)
        {
            if (DeployLightWasBlocked(drone))
                return null;

            var defaultAngle = _config.SearchLight.DefaultAngle - 90 % 360;
            var localRotation = Quaternion.Euler(defaultAngle, SearchLightYAxisRotation, 0);

            var searchLight = GameManager.server.CreateEntity(SearchLightPrefab, SearchLightLocalPosition + localRotation * SearchLightPivotPoint, localRotation) as SearchLight;
            if (searchLight == null)
                return null;

            SetupSearchLight(searchLight);

            searchLight.transform.localScale = SearchLightTransformScale;
            searchLight.networkEntityScale = true;

            searchLight.SetParent(drone);
            searchLight.Spawn();
            Interface.CallHook("OnDroneSearchLightDeployed", drone, searchLight);

            return searchLight;
        }

        private void SetupSearchLight(SearchLight searchLight)
        {
            RemoveProblemComponents(searchLight);
            HideInputsAndOutputs(searchLight);
            searchLight.EnableSaving(true);
            searchLight.SetFlag(BaseEntity.Flags.Busy, true);
            searchLight.baseProtection = _immortalProtection;
            searchLight.pickup.enabled = false;
        }

        private void AddOrUpdateSearchLight(Drone drone)
        {
            var searchLight = GetDroneSearchLight(drone, out var sphereEntity);
            if (searchLight == null)
            {
                MaybeAutoDeploySearchLight(drone);
                return;
            }

            // Remove the legacy sphere used to resize the search light.
            if (sphereEntity != null)
            {
                searchLight.SetParent(drone, worldPositionStays: true, sendImmediate: true);
                sphereEntity.Kill();
            }

            var searchLightTransform = searchLight.transform;
            if (!searchLight.networkEntityScale || searchLightTransform.localScale != SearchLightTransformScale)
            {
                searchLightTransform.localPosition = SearchLightLocalPosition + searchLightTransform.localRotation * SearchLightPivotPoint;
                searchLightTransform.localScale = SearchLightTransformScale;
                searchLight.networkEntityScale = true;
                searchLight.SendNetworkUpdate();
            }

            SetupSearchLight(searchLight);
        }

        private void MaybeAutoDeploySearchLight(Drone drone)
        {
            if (!permission.UserHasPermission(drone.OwnerID.ToString(), PermissionAutoDeploy))
                return;

            TryDeploySearchLight(drone);
        }

        #endregion

        #region Classes

        private class SearchLightUpdater : FacepunchBehaviour
        {
            public static void AddOrUpdateForDrone(DroneLights plugin, Drone drone, SearchLight searchLight, BasePlayer controller, bool canMove)
            {
                var component = GetForDrone(drone);
                if (component == null)
                {
                    component = drone.gameObject.AddComponent<SearchLightUpdater>();
                    component._plugin = plugin;
                    component._drone = drone;
                    component._searchLightTransform = searchLight.transform;
                    component._searchLight = searchLight;
                }

                component._controller = controller;
                component._canMove = canMove;
                component.enabled = true;
            }

            public static void RemoveFromDrone(Drone drone)
            {
                DestroyImmediate(GetForDrone(drone));
            }

            private static SearchLightUpdater GetForDrone(Drone drone)
            {
                return drone.gameObject.GetComponent<SearchLightUpdater>();
            }

            private DroneLights _plugin;
            private Drone _drone;
            private SearchLight _searchLight;
            private Transform _searchLightTransform;
            private BasePlayer _controller;
            private bool _canMove;

            private void Update()
            {
                var controllerSteamId = _drone.ControllingViewerId?.SteamId ?? 0;
                if (controllerSteamId == 0)
                {
                    enabled = false;
                    return;
                }

                _plugin.TrackStart();

                if (_controller.lastTickTime == Time.time && _controller.serverInput.WasJustPressed(BUTTON.FIRE_SECONDARY))
                {
                    _searchLight.SetFlag(IOEntity.Flag_HasPower, !_searchLight.IsPowered());
                }

                if (_canMove && !_drone.isGrounded && _searchLight.IsPowered())
                {
                    SetLightAngle(_drone, _searchLight, _searchLightTransform);
                }

                _plugin.TrackEnd();
            }
        }

        #endregion

        #region Configuration

        [JsonObject(MemberSerialization.OptIn)]
        private class Configuration : BaseConfiguration
        {
            [JsonProperty("SearchLight")]
            public SearchLightSettings SearchLight = new();
        }

        private class SearchLightSettings
        {
            [JsonProperty("DefaultAngle")]
            public int DefaultAngle = 75;
        }

        private Configuration GetDefaultConfig() => new();

        #region Configuration Helpers

        [JsonObject(MemberSerialization.OptIn)]
        private class BaseConfiguration
        {
            private string ToJson() => JsonConvert.SerializeObject(this);

            public Dictionary<string, object> ToDictionary() => JsonHelper.Deserialize(ToJson()) as Dictionary<string, object>;
        }

        private static class JsonHelper
        {
            public static object Deserialize(string json) => ToObject(JToken.Parse(json));

            private static object ToObject(JToken token)
            {
                switch (token.Type)
                {
                    case JTokenType.Object:
                        return token.Children<JProperty>()
                                    .ToDictionary(prop => prop.Name,
                                                  prop => ToObject(prop.Value));

                    case JTokenType.Array:
                        return token.Select(ToObject).ToList();

                    default:
                        return ((JValue)token).Value;
                }
            }
        }

        private bool MaybeUpdateConfig(BaseConfiguration config)
        {
            var currentWithDefaults = config.ToDictionary();
            var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value);
            return MaybeUpdateConfigSection(currentWithDefaults, currentRaw);
        }

        private bool MaybeUpdateConfigSection(Dictionary<string, object> currentWithDefaults, Dictionary<string, object> currentRaw)
        {
            var changed = false;

            foreach (var key in currentWithDefaults.Keys)
            {
                if (currentRaw.TryGetValue(key, out var currentRawValue))
                {
                    var defaultDictValue = currentWithDefaults[key] as Dictionary<string, object>;
                    var currentDictValue = currentRawValue as Dictionary<string, object>;

                    if (defaultDictValue != null)
                    {
                        if (currentDictValue == null)
                        {
                            currentRaw[key] = currentWithDefaults[key];
                            changed = true;
                        }
                        else if (MaybeUpdateConfigSection(defaultDictValue, currentDictValue))
                            changed = true;
                    }
                }
                else
                {
                    currentRaw[key] = currentWithDefaults[key];
                    changed = true;
                }
            }

            return changed;
        }

        protected override void LoadDefaultConfig() => _config = GetDefaultConfig();

        protected override void LoadConfig()
        {

            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Configuration>();
                if (_config == null)
                {
                    throw new JsonException();
                }

                if (MaybeUpdateConfig(_config))
                {
                    LogWarning("Configuration appears to be outdated; updating and saving");
                    SaveConfig();
                }
            }
            catch (Exception e)
            {
                LogError(e.Message);
                LogWarning($"Configuration file {Name}.json is invalid; using defaults");
                LoadDefaultConfig();
            }
        }

        protected override void SaveConfig()
        {
            Puts($"Configuration changes saved to {Name}.json");
            Config.WriteObject(_config, true);
        }

        #endregion

        #endregion
    }
}
