using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Facepunch;
using Newtonsoft.Json;
using Oxide.Core;
using Rust;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Random Respawner", "Egor Blagov/Arainrr/Tryhard", "1.2.8")]
    [Description("Plugin respawns player in random place")]
    internal class RandomRespawner : RustPlugin
    {
        private const string PERMISSION_USE = "randomrespawner.use";
        private Coroutine findSpawnPosCoroutine;
        private readonly List<Collider> colliders = new List<Collider>();
        private readonly List<Vector3> spawnPositionCache = new List<Vector3>();

        private BasePlayer.SpawnPoint reusableSpawnPoint = new BasePlayer.SpawnPoint
        {
            pos = Vector3.zero,
            rot = Quaternion.identity
        };

        #region Oxide Hooks

        private void Init()
        {
            permission.RegisterPermission(PERMISSION_USE, this);
        }

        private void OnServerInitialized()
        {
            UpdateConfig();
            findSpawnPosCoroutine = ServerMgr.Instance.StartCoroutine(FindSpawnPositions(5000));
        }

        private void Unload()
        {
            if (findSpawnPosCoroutine != null)
            {
                ServerMgr.Instance.StopCoroutine(findSpawnPosCoroutine);
            }
        }

        private object OnPlayerRespawn(BasePlayer player)
        {
            if (player == null || !player.userID.IsSteamId()) return null;
            if (!permission.UserHasPermission(player.UserIDString, PERMISSION_USE))
            {
                return null;
            }

            var spawnPos = GetRandomSpawnPos();
            if (!spawnPos.HasValue)
            {
                PrintWarning("Unable to generate random respawn position, try limit exceed, spawn as default");
                return null;
            }

            if (Interface.CallHook("OnRandomRespawn", player, spawnPos.Value) != null)
            {
                return null;
            }
            
            reusableSpawnPoint.pos = spawnPos.Value;
            return reusableSpawnPoint;
        }

        #endregion Oxide Hooks

        #region Methods

        private IEnumerator FindSpawnPositions(int attempts = 1500)
        {
            List<Vector3> list = Pool.Get<List<Vector3>>();
            float mapSizeX = TerrainMeta.Size.x / 2;
            float mapSizeZ = TerrainMeta.Size.z / 2;
            Vector3 randomPos = Vector3.zero;
            for (int i = 0; i < attempts; i++)
            {
                randomPos.x = UnityEngine.Random.Range(-mapSizeX, mapSizeX);
                randomPos.z = UnityEngine.Random.Range(-mapSizeZ, mapSizeZ);
                if (TestPos(ref randomPos))
                {
                    list.Add(randomPos);
                }

                if (i % 20 == 0)
                {
                    yield return CoroutineEx.waitForEndOfFrame;
                }
            }
            spawnPositionCache.AddRange(list);
            PrintWarning($"Successfully found {list.Count} spawn positions.");
            Pool.FreeUnmanaged(ref list);
            findSpawnPosCoroutine = null;
        }

        private Vector3? GetRandomSpawnPos()
        {
            for (int i = 0; i < configData.maxAttempts; i++)
            {
                if (spawnPositionCache.Count <= 0) return null;
                var spawnPos = spawnPositionCache.GetRandom();
                if (TestPosAgain(spawnPos))
                {
                    return spawnPos;
                }
                spawnPositionCache.Remove(spawnPos);
                if (spawnPositionCache.Count < 50 && findSpawnPosCoroutine == null)
                {
                    findSpawnPosCoroutine = ServerMgr.Instance.StartCoroutine(FindSpawnPositions());
                }
            }
            
            return null;
        }

        private bool TestPos(ref Vector3 randomPos)
        {
            RaycastHit hitInfo;
            if (!Physics.Raycast(randomPos + Vector3.up * 300f, Vector3.down, out hitInfo, 400f, Layers.Solid) ||
                hitInfo.GetEntity() != null)
            {
                return false;
            }

            var collider = hitInfo.collider;
            if (collider != null && collider.material.name.Contains("rock", CompareOptions.OrdinalIgnoreCase))
            {
                return false;
            }
            
            randomPos.y = hitInfo.point.y + 0.1f;

            var slope = GetPosSlope(randomPos);
            if (slope < configData.minSlope || slope > configData.maxSlope)
            {
                return false;
            }

            bool enabled;
            var biome = GetPosBiome(randomPos);
            if (configData.biomes.TryGetValue(biome, out enabled) && !enabled)
            {
                return false;
            }

            var splat = GetPosSplat(randomPos);
            if (configData.splats.TryGetValue(splat, out enabled) && !enabled)
            {
                return false;
            }

            var topology = GetPosTopology(randomPos);
            if (configData.topologies.TryGetValue(topology, out enabled) && !enabled)
            {
                return false;
            }
            
            if (AntiHack.TestInsideTerrain(randomPos))
            {
                return false;
            }
            
            if (!VBounds(randomPos))
            {
                return false;
            }
            
            return TestPosAgain(randomPos);
        }

        private bool VBounds(Vector3 pos)
        {
            return !SingletonComponent<ValidBounds>.Instance || IsInside(pos);
        }

        internal bool IsInside(Vector3 vPos)
        {
            if (vPos.IsNaNOrInfinity())
            {
                return false;
            }
            if (!SingletonComponent<ValidBounds>.Instance.worldBounds.Contains(vPos))
            {
                return false;
            }
            if (TerrainMeta.HeightMap != null)
            {
                if (World.Procedural && vPos.y < TerrainMeta.Position.y)
                {
                    return false;
                }
                if (TerrainMeta.OutOfMargin(vPos))
                {
                    return false;
                }
            }
            
            return true;
        }

        private bool TestPosAgain(Vector3 spawnPos)
        {
            if (WaterLevel.Test(spawnPos, true, true))
            {
                return false;
            }

            colliders.Clear();
            Vis.Colliders(spawnPos, 3f, colliders);
            foreach (var collider in colliders)
            {
                switch (collider.gameObject.layer)
                {
                    case (int)Layer.Prevent_Building:
                        if (configData.preventSpawnAtMonument)
                        {
                            return false;
                        }
                        break;

                    case (int)Layer.Vehicle_Large: //cargoshiptest
                    case (int)Layer.Vehicle_World:
                    case (int)Layer.Vehicle_Detailed:
                        return false;
                }

                if (configData.preventSpawnAtZone && collider.name.Contains("zonemanager", CompareOptions.IgnoreCase))
                {
                    return false;
                }

                if (configData.preventSpawnAtRadZone && collider.name.Contains("radiation", CompareOptions.IgnoreCase))
                {
                    return false;
                }

                if (collider.name.Contains("fireball", CompareOptions.IgnoreCase) ||
                    collider.name.Contains("iceberg", CompareOptions.IgnoreCase) ||
                    collider.name.Contains("ice_sheet", CompareOptions.IgnoreCase))
                {
                    return false;
                }
            }

            if (configData.radiusFromPlayers > 0)
            {
                var players = Pool.Get<List<BasePlayer>>();
                Vis.Entities(spawnPos, configData.radiusFromPlayers, players, Layers.Mask.Player_Server);
                foreach (var player in players)
                {
                    if (!player.IsSleeping())
                    {
                        Pool.FreeUnmanaged(ref players);
                        return false;
                    }
                }
                Pool.FreeUnmanaged(ref players);
            }
            if (configData.radiusFromBuilding > 0)
            {
                var entities = Pool.Get<List<BaseEntity>>();
                Vis.Entities(spawnPos, configData.radiusFromBuilding, entities, Layers.PlayerBuildings);
                if (entities.Any())
                {
                    Pool.FreeUnmanaged(ref entities);
                    return false;
                }
                Pool.FreeUnmanaged(ref entities);
            }

            return true;
        }

        private void UpdateConfig()
        {
            foreach (TerrainBiome.Enum value in Enum.GetValues(typeof(TerrainBiome.Enum)))
            {
                configData.biomes.TryAdd(value, true);
            }
            foreach (TerrainSplat.Enum value in Enum.GetValues(typeof(TerrainSplat.Enum)))
            {
                configData.splats.TryAdd(value, true);
            }
            foreach (TerrainTopology.Enum value in Enum.GetValues(typeof(TerrainTopology.Enum)))
            {
                configData.topologies.TryAdd(value, true);
            }
            SaveConfig();
        }

        #endregion Methods

        #region Helpers

        private static float GetPosSlope(Vector3 position) => TerrainMeta.HeightMap.GetSlope(position);

        private static TerrainBiome.Enum GetPosBiome(Vector3 position) => (TerrainBiome.Enum)TerrainMeta.BiomeMap.GetBiomeMaxType(position);

        private static TerrainSplat.Enum GetPosSplat(Vector3 position) => (TerrainSplat.Enum)TerrainMeta.SplatMap.GetSplatMaxType(position);

        private static TerrainTopology.Enum GetPosTopology(Vector3 position) => (TerrainTopology.Enum)TerrainMeta.TopologyMap.GetTopology(position);

        #endregion Helpers

        #region ConfigurationFile

        private ConfigData configData;

        private class ConfigData
        {
            [JsonProperty(PropertyName = "Maximum Attempts To Find A Respawn Position")]
            public int maxAttempts = 200;

            [JsonProperty(PropertyName = "Minimum Distance From Other Players (Including NPC Players)")]
            public float radiusFromPlayers = 20.0f;

            [JsonProperty(PropertyName = "Minimum Distance From Building")]
            public float radiusFromBuilding = 20.0f;

            [JsonProperty(PropertyName = "Prevent Players To Be Respawn At Monuments")]
            public bool preventSpawnAtMonument = true;

            [JsonProperty(PropertyName = "Prevent Players To Be Respawn At ZoneManager")]
            public bool preventSpawnAtZone = true;

            [JsonProperty(PropertyName = "Prevent Players To Be Respawn At RadiationZone")]
            public bool preventSpawnAtRadZone = true;

            [JsonProperty(PropertyName = "Minimum Slope")]
            public float minSlope = 0f;

            [JsonProperty(PropertyName = "Maximum Slope")]
            public float maxSlope = 60f;

            [JsonProperty(PropertyName = "Biome Settings")]
            public Dictionary<TerrainBiome.Enum, bool> biomes = new Dictionary<TerrainBiome.Enum, bool>();

            [JsonProperty(PropertyName = "Splat Settings")]
            public Dictionary<TerrainSplat.Enum, bool> splats = new Dictionary<TerrainSplat.Enum, bool>();

            [JsonProperty(PropertyName = "Topology Settings")]
            public Dictionary<TerrainTopology.Enum, bool> topologies = new Dictionary<TerrainTopology.Enum, bool>();
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                configData = Config.ReadObject<ConfigData>();
                if (configData == null)
                    LoadDefaultConfig();
            }
            catch (Exception ex)
            {
                PrintError($"The configuration file is corrupted. \n{ex}");
                LoadDefaultConfig();
            }

            SaveConfig();
        }

        protected override void LoadDefaultConfig()
        {
            PrintWarning("Creating a new configuration file");
            configData = new ConfigData();
        }

        protected override void SaveConfig() => Config.WriteObject(configData);

        #endregion ConfigurationFile
    }
}