using Facepunch;
using Oxide.Core;
using Oxide.Core.Libraries;
using Newtonsoft.Json;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using UnityEngine;
using UnityEngine.AI;

#region Notes
/**********************************************************************************************
* 1.1.1 :
* Removed Linq usage.
* Improved Grid Calculations to be more accurate.
* Rebuild the waterchecks and will teleport the player to the closest shore inside a 100 radius.
* 
**********************************************************************************************/
#endregion

namespace Oxide.Plugins
{
    [Info("Gr Teleport" , "carny666 & Krungh Crow" , "1.1.1")]
    [Description("Allows teleportation using the map grid reference and death reference")]
    class GrTeleport : RustPlugin
    {
        #region constants

        const float DefaultGridCellSize = 150f;
        const float AboveGroundPosition = 2.5f;

        #endregion

        #region permissions

        private const string adminPermission = "GrTeleport.admin";
        private const string grtPermission = "GrTeleport.use";

        #endregion

        #region local variables / supporting classes

        GrTeleportData grTeleportData;

        int lastGridTested = 0;
        List<SpawnPosition> spawnGrid = new List<SpawnPosition>();
        List<Cooldown> coolDowns = new List<Cooldown>();
        List<RustUser> rustUsers = new List<RustUser>();
        List<DeathNotice> pendingDeathNotice = new List<DeathNotice>();

        private bool debug = false;

        class GrTeleportData
        {
            public int CooldownInSeconds { get; set; }
            public bool AvoidWater { get; set; }
            public bool AllowBuildingBlocked { get; set; }
            public bool AllowOffCargoship { get; set; }
            public int LimitPerDay { get; set; }
            public string RestrictedZones { get; set; }
            public int DistanceToWarnConstructions { get; set; }
            public float AllowableWaterDepth { get; set; }
            public bool DisplayDeathLocation { get; set; }
            public int SecondsBeforeTeleport { get; set; }
            public float MinHealthForTeleport { get; set; }
            public float MinThirstForTeleport { get; set; }
            public float MinHungerForTeleport { get; set; }
            public float MinTemperatureForTeleport { get; set; }
            public float MaxTemperatureForTeleport { get; set; }
            public Dictionary<string , GroupData> groupData = new Dictionary<string , GroupData>();
        }

        class SpawnPosition
        {
            public Vector3 Position;
            public Vector3 GroundPosition;
            public Vector3 LastTeleportPosition;
            public string GridReference;

            public float MinX;
            public float MaxX;
            public float MinZ;
            public float MaxZ;

            public SpawnPosition(string gridReference , float minX , float maxX , float minZ , float maxZ)
            {
                GridReference = gridReference;
                MinX = minX;
                MaxX = maxX;
                MinZ = minZ;
                MaxZ = maxZ;

                float width = maxX - minX;
                float height = maxZ - minZ;

                Position = new Vector3(minX + (width * 0.20f) , 0f , minZ + (height * 0.20f));
                GroundPosition = Vector3.zero;
                LastTeleportPosition = Vector3.zero;
            }

            public Vector3 GetReferencePosition()
            {
                if (GroundPosition != Vector3.zero)
                    return GroundPosition;

                return Position;
            }

            public bool IsPositionInWater()
            {
                Vector3 pos = GetReferencePosition();
                return TerrainMeta.WaterMap.GetHeight(pos) > TerrainMeta.HeightMap.GetHeight(pos);
            }

            public bool IsPositionAboveWater()
            {
                return !IsPositionInWater();
            }

            public float WaterDepthAtPosition()
            {
                Vector3 pos = GetReferencePosition();
                return TerrainMeta.WaterMap.GetHeight(pos) - TerrainMeta.HeightMap.GetHeight(pos);
            }

            public static Vector3 GetGroundPosition(Vector3 position , float aboveGroundPosition , bool inWater , GrTeleportData grTeleportData)
            {
                if (inWater && grTeleportData.AvoidWater)
                    return Vector3.zero;

                NavMeshHit hit;
                if (NavMesh.SamplePosition(position , out hit , 20f , NavMesh.AllAreas))
                    return hit.position + new Vector3(0f , aboveGroundPosition , 0f);

                RaycastHit rayHit;
                if (Physics.Raycast(position + new Vector3(0f , 50f , 0f) , Vector3.down , out rayHit , 100f , Rust.Layers.Terrain))
                    return rayHit.point + new Vector3(0f , aboveGroundPosition , 0f);

                return Vector3.zero;
            }
        }

        bool IsGroundCloseEnough(Vector3 candidate , Vector3 ground , float maxDistance)
        {
            Vector2 a = new Vector2(candidate.x , candidate.z);
            Vector2 b = new Vector2(ground.x , ground.z);
            return Vector2.Distance(a , b) <= maxDistance;
        }

        class Cooldown
        {
            public string name;
            public int cooldownPeriodSeconds;
            public DateTime lastUse;
            public DateTime expirtyDateTime;

            public Cooldown(string playerName , int cooldownInSeconds)
            {
                name = playerName;
                cooldownPeriodSeconds = cooldownInSeconds;
                lastUse = DateTime.Now;
                expirtyDateTime = lastUse.AddSeconds(cooldownPeriodSeconds);
            }
        }

        class RustUser
        {
            public BasePlayer Player { get; set; }
            public int TeleportsRemaining { get; set; }
            public DateTime ResetDateTime { get; set; }
            public int CoolDownInSeconds { get; set; }
            public bool isWaitingForTeleport { get; set; }
        }

        class GroupData
        {
            public int coolDownPeriodSeconds { get; set; }
            public int dailyTeleports { get; set; }
            public string groupName { get; set; }
            public int SecondsBeforeTeleport { get; set; }
            public float MinHealthForTeleport { get; set; }
            public float MinThirstForTeleport { get; set; }
            public float MinHungerForTeleport { get; set; }
            public float MinTemperatureForTeleport { get; set; }
            public float MaxTemperatureForTeleport { get; set; }
        }

        class DeathNotice
        {
            public BasePlayer Player { get; set; }
            public Vector3 DeathLocation { get; set; }
            public string DeathGridReference { get; set; }
        }

        #endregion

        #region events

        void Init()
        {
            grTeleportData = Config.ReadObject<GrTeleportData>();

            if (Config["Messages"] != null)
                Config.WriteObject(grTeleportData , true);

            permission.RegisterPermission(adminPermission , this);
            permission.RegisterPermission(grtPermission , this);
        }

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string , string>
            {
                { "buildingblocked", "You cannot grTeleport into or out from a building blocked area." },
                { "cargoship", "You cannot grTeleport off the Cargoship." },
                { "CannotGridTeleport", "You cannot grTeleport to this position." },
                { "noinit", "spawnGrid was not initialized. 0 spawn points available." },
                { "teleported", "You have GrTeleported to {gridreference}, {position}. You're {groupname}, so you have a {cooldown} second cooldown between uses and {limit} grTeleports." },
                { "teleportlimit", "You have {teleportsremaining} remaining." },
                { "teleportminutesexhauseted", "You have exhausted your remaining grTeleports for today. You can use grTeleport in {minutesleft} minutes." },
                { "teleporthoursexhauseted", "You have exhausted your remaining grTeleports for today. You can use grTeleport in {hoursleft} hours." },
                { "overwater", "That reference point is above water." },
                { "cmdusage", "usage ex: /grt n10  (where n = a-zz and 10=0-60" },
                { "noaccess", "You do not have sufficient access to execute this command." },
                { "sgerror", "Error creating spawnpoints, too much water? contact dev." },
                { "cooldown", "Sorry, your are currently in a {cooldownperiod} second cooldown, you have another {secondsleft} seconds remaining." },
                { "cooldownreply", "Cooldown has been set to {cooldownperiod} seconds" },
                { "gridwidthreply", "Gridwidth has been set to {gridwidth}x{gridwidth}" },
                { "cuboardreply", "Buidling block teleportation is {togglebuildingblocked}" },
                { "cargoreply", "Cargoship teleportation is {togglecargoship}" },
                { "avoidwaterreply", "Avoid water has been set tp {avoidwater}" },
                { "dailylimitreply", "Daily limit has been set to {dailylimit}." },
                { "cupboard", "Sorry, you cannot teleport within {distance} of a cupboard." },
                { "zoneadded", "Restricted Zone ({zone}) has been added, you now have {zones}." },
                { "zonenotadded", "You need to enter a zone (or a comma seperated list of zones) as well." },
                { "restrictedzone", "You cannot teleport here, {zone} is restricted." },
                { "zonecomma", "You need to supply a commaseperated list of zones." },
                { "zonesadded", "Restricted Zones ({zone}) have been added, you now have {zones}." },
                { "zonenotremoved", "Restricted Zone ({zone}) has not been removed, you now have {zones}." },
                { "zoneremoved", "Restricted Zone ({zone}) have been removed, you now have {zones}." },
                { "zonesremoved", "Restricted Zones ({zone}) have been removed, you now have {zones}." },
                { "nozones", "There are no zones to remove." },
                { "buildonref", "Your construction is within the vicinty of a grTransport reference. You may want to reconsider building here within {amount}m. your are now {distance}." },
                { "constructionreply", "Construction/Grid Reference distance has been set to {DistanceToWarnConstructions}m." },
                { "setgroupusage", "type /setgroup groupName 30 10 - where 30 is cooldown and 10 is daily teleport limit." },
                { "setgroupusageerror", "Must have 3 arguments. /setgroup groupName 30 10" },
                { "setwaterdepthreply", "Allowable water depth has been set to {waterdepth}" },
                { "setwaterdeptherror", "usage: /setwaterdepth 1.0" },
                { "togglecargoshipreply", "Cargoship has been toggled to {displayCargoship}" },
                { "toggleDeathMessagereply", "Death message has been toggled to {displayDeath}" },
                { "clearingallzones", "Clearing all zones from restricted." },
                { "addingallzones", "Adding all zones as restricted." },
                { "delaymessage", "You will be teleported in {seconds} seconds." },
                { "thirstymessage", "You are too thirsty to be teleported, get some water." },
                { "hungrymessage", "You are too hungry to be teleported, get some food." },
                { "woundedmessage", "You wounded, you cannot be teleported, get some medical attention." },
                { "coldmessage", "You are too cold ({temperature}) to be teleported, get warmer." },
                { "hotmessage", "You are too hot ({temperature}>{toohot}) to be teleported, cool off." },
                { "radiatedmessage", "You are radiated, you cannot to be teleported, find some meds or water." },
                { "alreadywaitingmessage", "You are already waiting to teleport." },
                { "nogroups", "There are not groups to list." }
            } , this);
        }

        void OnEntityBuilt(Planner plan , GameObject go)
        {
            try
            {
                BasePlayer owner = plan.GetOwnerPlayer();
                if (owner == null)
                    return;

                var amount = grTeleportData.DistanceToWarnConstructions;
                var playerGround = GetActualGroundPosition(owner.transform.position);

                for (int i = 0; i < spawnGrid.Count; i++)
                {
                    SpawnPosition sp = spawnGrid[i];
                    Vector3 refPos = sp.GetReferencePosition();

                    float distance = Vector3.Distance(refPos , playerGround);
                    if (distance <= amount)
                    {
                        PrintToChat(owner , lang.GetMessage("buildonref" , this , owner.UserIDString)
                            .Replace("{amount}" , amount.ToString())
                            .Replace("{distance}" , distance.ToString("0.0")));
                        return;
                    }
                }
            }
            catch (Exception ex)
            {
                PrintError(ex.Message);
            }
        }

        protected override void LoadDefaultConfig()
        {
            var data = new GrTeleportData
            {
                CooldownInSeconds = 15 ,
                AvoidWater = true ,
                AllowBuildingBlocked = false ,
                AllowOffCargoship = false ,
                LimitPerDay = -1 ,
                RestrictedZones = "ZZZ123,YYY666" ,
                DistanceToWarnConstructions = 15 ,
                AllowableWaterDepth = 1.0f ,
                DisplayDeathLocation = true ,
                SecondsBeforeTeleport = 10 ,
                MinHealthForTeleport = 40 ,
                MinThirstForTeleport = 40 ,
                MinTemperatureForTeleport = 0 ,
                MaxTemperatureForTeleport = 40 ,
                MinHungerForTeleport = 40 ,
                groupData = new Dictionary<string , GroupData>()
            };

            Config.WriteObject(data , true);
        }

        void OnEntityDeath(BaseEntity entity , HitInfo info)
        {
            try
            {
                if (!grTeleportData.DisplayDeathLocation)
                    return;

                BasePlayer player = entity as BasePlayer;
                if (player == null)
                    return;

                pendingDeathNotice.Add(new DeathNotice
                {
                    Player = player ,
                    DeathLocation = entity.transform.position ,
                    DeathGridReference = GetGridReference(entity.transform.position)
                });
            }
            catch (Exception ex)
            {
                PrintError("Error OnEntityDeath " + ex.StackTrace);
            }
        }

        void OnPlayerRespawned(BasePlayer player)
        {
            if (!grTeleportData.DisplayDeathLocation)
                return;

            DeathNotice deathNotice = null;

            for (int i = 0; i < pendingDeathNotice.Count; i++)
            {
                if (pendingDeathNotice[i].Player == player)
                {
                    deathNotice = pendingDeathNotice[i];
                    break;
                }
            }

            if (deathNotice != null)
            {
                pendingDeathNotice.Remove(deathNotice);
                PrintToChat(player , "You died " + deathNotice.DeathGridReference);
            }
        }

        void OnServerInitialized()
        {
            timer.Once(0.1f , () =>
            {
                spawnGrid = CreateSpawnGrid();
            });
        }

        #endregion

        #region commands

        [ChatCommand("grt")]
        void ChatCommandGrt(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (args.Length > 0)
                {
                    string gridReference = args[0];
                    GroupData userGroupData = GetUsersGroupDataOrDefault(player);

                    if (userGroupData == null && !player.IsAdmin)
                    {
                        CheckAccess(player , "grt" , "userGroupDataNull" , true);
                        return;
                    }

                    RustUser user = GetOrCreateUser(player , userGroupData);

                    if (user.isWaitingForTeleport && !player.IsAdmin)
                    {
                        PrintToChat(player , lang.GetMessage("alreadywaitingmessage" , this , player.UserIDString));
                        return;
                    }

                    if (user.TeleportsRemaining == 0 && !player.IsAdmin)
                    {
                        TimeSpan resetIn = user.ResetDateTime - DateTime.Now;
                        if (resetIn.TotalHours < 1)
                        {
                            PrintToChat(player , lang.GetMessage("teleportminutesexhauseted" , this , player.UserIDString)
                                .Replace("{minutesleft}" , resetIn.TotalMinutes.ToString("0"))
                                .Replace("{hoursleft}" , resetIn.TotalHours.ToString("0")));
                        }
                        else
                        {
                            PrintToChat(player , lang.GetMessage("teleporthoursexhauseted" , this , player.UserIDString)
                                .Replace("{minutesleft}" , resetIn.TotalMinutes.ToString("0"))
                                .Replace("{hoursleft}" , resetIn.TotalHours.ToString("0")));
                        }

                        return;
                    }

                    Cooldown tmp = GetCooldown(player.displayName);
                    if (tmp != null && !player.IsAdmin)
                    {
                        PrintToChat(player , lang.GetMessage("cooldown" , this , player.UserIDString)
                            .Replace("{cooldownperiod}" , tmp.cooldownPeriodSeconds.ToString())
                            .Replace("{secondsleft}" , tmp.expirtyDateTime.Subtract(DateTime.Now).TotalSeconds.ToString("0")));
                        return;
                    }

                    if (TestRestrictedZone(gridReference) && !player.IsAdmin)
                    {
                        PrintToChat(player , lang.GetMessage("restrictedzone" , this , player.UserIDString)
                            .Replace("{zone}" , gridReference.ToUpper()));
                        return;
                    }

                    if (spawnGrid == null || spawnGrid.Count <= 0)
                        spawnGrid = CreateSpawnGrid();

                    if (!TestCanGrtTeleport(player , gridReference))
                        return;

                    int delay = player.IsAdmin ? 0 : userGroupData.SecondsBeforeTeleport;

                    if (delay > 1)
                        PrintToChat(player , lang.GetMessage("delaymessage" , this , player.UserIDString).Replace("{seconds}" , delay.ToString()));

                    user.isWaitingForTeleport = true;

                    timer.Once(delay , () =>
                    {
                        user.isWaitingForTeleport = false;

                        if (player == null || !player.IsConnected)
                            return;

                        if (!TestCanGrtTeleport(player , gridReference))
                            return;

                        if (TeleportToGridReference(player , gridReference , false))
                        {
                            if (user.TeleportsRemaining != -1)
                                user.TeleportsRemaining--;

                            PrintToChat(player , lang.GetMessage("teleported" , this , player.UserIDString)
                                .Replace("{position}" , player.transform.position.ToString())
                                .Replace("{gridreference}" , gridReference.ToUpper())
                                .Replace("{groupname}" , userGroupData.groupName)
                                .Replace("{cooldown}" , userGroupData.coolDownPeriodSeconds.ToString())
                                .Replace("{limit}" , userGroupData.dailyTeleports == -1 ? "unlimited" : userGroupData.dailyTeleports.ToString()));

                            if (user.TeleportsRemaining != -1)
                            {
                                PrintToChat(player , lang.GetMessage("teleportlimit" , this , player.UserIDString)
                                    .Replace("{teleportsremaining}" , user.TeleportsRemaining.ToString()));
                            }

                            AddToCoolDown(player.displayName , userGroupData.coolDownPeriodSeconds);
                        }
                    });

                    return;
                }
            }
            catch (Exception ex)
            {
                PrintError("Error chatCommandGrt:" + ex.Message);
                return;
            }

            PrintToChat(player , lang.GetMessage("cmdusage" , this , player.UserIDString));
        }

        [ConsoleCommand("grt.testpos")]
        void CcGrtTestPos(ConsoleSystem.Arg arg)
        {
            try
            {
                if (arg.Player() == null)
                    return;

                BasePlayer player = arg.Player();

                if (!CheckAccess(player , "grt.testpos" , adminPermission))
                    return;

                PrintToChat(player , GetGridReference(player.transform.position));
            }
            catch (Exception ex)
            {
                throw new Exception("ccGrtTestPos " + ex.Message);
            }
        }

        [ConsoleCommand("grt.nextspawn")]
        void CcGrtNextspawn(ConsoleSystem.Arg arg)
        {
            try
            {
                if (arg.Player() == null)
                    return;

                BasePlayer player = arg.Player();

                if (spawnGrid.Count <= 0)
                    throw new Exception(lang.GetMessage("noinit" , this , player.UserIDString));

                if (!CheckAccess(player , "grt.nextspawn" , adminPermission))
                    return;

                int checkedCount = 0;
                Vector3 destination = Vector3.zero;
                SpawnPosition chosen = null;

                while (checkedCount < spawnGrid.Count)
                {
                    if (++lastGridTested >= spawnGrid.Count)
                        lastGridTested = 0;

                    chosen = spawnGrid[lastGridTested];
                    destination = FindValidPositionInGridCell(null , chosen , false , false);

                    if (destination != Vector3.zero)
                        break;

                    checkedCount++;
                }

                if (chosen == null || destination == Vector3.zero)
                {
                    PrintToChat(player , lang.GetMessage("CannotGridTeleport" , this , player.UserIDString));
                    return;
                }

                chosen.GroundPosition = destination;
                chosen.LastTeleportPosition = destination;

                player.SetParent(null , 0);
                player.EnsureDismounted();
                player.Teleport(destination);

                PrintToChat(player , lang.GetMessage("teleported" , this , player.UserIDString)
                    .Replace("{gridreference}" , chosen.GridReference)
                    .Replace("{position}" , player.transform.position.ToString()));
            }
            catch (Exception ex)
            {
                throw new Exception("ccGrtNextspawn " + ex.Message);
            }
        }

        [ConsoleCommand("grt.prevspawn")]
        void CcGrtPrevspawn(ConsoleSystem.Arg arg)
        {
            try
            {
                if (arg.Player() == null)
                    return;

                BasePlayer player = arg.Player();

                if (spawnGrid.Count <= 0)
                    throw new Exception(lang.GetMessage("noinit" , this , player.UserIDString));

                if (!CheckAccess(player , "grt.prevspawn" , adminPermission))
                    return;

                int checkedCount = 0;
                Vector3 destination = Vector3.zero;
                SpawnPosition chosen = null;

                while (checkedCount < spawnGrid.Count)
                {
                    if (--lastGridTested < 0)
                        lastGridTested = spawnGrid.Count - 1;

                    chosen = spawnGrid[lastGridTested];
                    destination = FindValidPositionInGridCell(null , chosen , false , false);

                    if (destination != Vector3.zero)
                        break;

                    checkedCount++;
                }

                if (chosen == null || destination == Vector3.zero)
                {
                    PrintToChat(player , lang.GetMessage("CannotGridTeleport" , this , player.UserIDString));
                    return;
                }

                chosen.GroundPosition = destination;
                chosen.LastTeleportPosition = destination;

                player.SetParent(null , 0);
                player.EnsureDismounted();
                player.Teleport(destination);

                PrintToChat(player , lang.GetMessage("teleported" , this , player.UserIDString)
                    .Replace("{gridreference}" , chosen.GridReference)
                    .Replace("{position}" , player.transform.position.ToString()));
            }
            catch (Exception ex)
            {
                throw new Exception("ccGrtPrevspawn " + ex.Message);
            }
        }

        [ChatCommand("setcooldown")]
        void ChmSetCooldown(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "setcooldown" , adminPermission))
                    return;

                if (args.Length > 0)
                    grTeleportData.CooldownInSeconds = int.Parse(args[0]);

                coolDowns.Clear();

                Config.WriteObject(grTeleportData , true);

                PrintToChat(player , lang.GetMessage("cooldownreply" , this , player.UserIDString)
                    .Replace("{cooldownperiod}" , grTeleportData.CooldownInSeconds.ToString()));
            }
            catch (Exception ex)
            {
                throw new Exception("chmSetCooldown " + ex.Message);
            }
        }

        [ChatCommand("setwaterdepth")]
        void ChmSetWaterDepth(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "setwaterdepth" , adminPermission))
                    return;

                try
                {
                    if (args.Length > 0)
                        grTeleportData.AllowableWaterDepth = float.Parse(args[0]);
                }
                catch
                {
                    PrintToChat(player , lang.GetMessage("setwaterdeptherror" , this , player.UserIDString)
                        .Replace("{waterdepth}" , grTeleportData.AllowableWaterDepth.ToString("0.00")));
                }

                Config.WriteObject(grTeleportData , true);

                PrintToChat(player , lang.GetMessage("setwaterdepthreply" , this , player.UserIDString)
                    .Replace("{waterdepth}" , grTeleportData.AllowableWaterDepth.ToString("0.00")));
            }
            catch (Exception ex)
            {
                throw new Exception("chmSetWaterDepth " + ex.Message);
            }
        }

        [ChatCommand("toggledeathmessage")]
        void ChmToggleDeathMessage(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "toggleavoidwater" , adminPermission))
                    return;

                grTeleportData.DisplayDeathLocation = !grTeleportData.DisplayDeathLocation;
                Config.WriteObject(grTeleportData , true);

                PrintToChat(player , lang.GetMessage("toggleDeathMessagereply" , this , player.UserIDString)
                    .Replace("{displayDeath}" , grTeleportData.DisplayDeathLocation.ToString()));
            }
            catch (Exception ex)
            {
                throw new Exception("chmToggleDeathMessage " + ex.Message);
            }
        }

        [ChatCommand("setconstruction")]
        void ChmSetConstruction(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "setconstruction" , adminPermission))
                    return;

                if (args.Length > 0)
                    grTeleportData.DistanceToWarnConstructions = int.Parse(args[0]);

                coolDowns.Clear();

                Config.WriteObject(grTeleportData , true);

                PrintToChat(player , lang.GetMessage("constructionreply" , this , player.UserIDString)
                    .Replace("{DistanceToWarnConstructions}" , grTeleportData.DistanceToWarnConstructions.ToString()));
            }
            catch (Exception ex)
            {
                throw new Exception("chmSetConstruction " + ex.Message);
            }
        }

        [ChatCommand("setdailylimit")]
        void ChmSetDailyLimit(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "setdailylimit" , adminPermission))
                    return;

                if (args.Length > 0)
                    grTeleportData.LimitPerDay = int.Parse(args[0]);

                rustUsers.Clear();

                Config.WriteObject(grTeleportData , true);

                PrintToChat(player , lang.GetMessage("dailylimitreply" , this , player.UserIDString)
                    .Replace("{dailylimit}" , grTeleportData.LimitPerDay.ToString()));
            }
            catch (Exception ex)
            {
                throw new Exception("chmSetDailyLimit " + ex.Message);
            }
        }

        [ChatCommand("addzone")]
        void ChmAddZone(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "chmAddzone" , adminPermission))
                    return;

                if (args.Length > 0)
                {
                    if (string.IsNullOrEmpty(grTeleportData.RestrictedZones))
                        grTeleportData.RestrictedZones = "";

                    if (args[0].Contains(","))
                    {
                        string[] split = args[0].ToUpper().Split(',');
                        for (int i = 0; i < split.Length; i++)
                        {
                            string z = split[i];
                            if (grTeleportData.RestrictedZones.Length > 0)
                                grTeleportData.RestrictedZones += "," + z;
                            else
                                grTeleportData.RestrictedZones += z;
                        }

                        Config.WriteObject(grTeleportData , true);

                        PrintToChat(player , lang.GetMessage("zoneadded" , this , player.UserIDString)
                            .Replace("{zone}" , args[0].ToUpper())
                            .Replace("{zones}" , grTeleportData.RestrictedZones.Replace("ZZZ123,YYY666," , "")));
                    }
                    else
                    {
                        if (grTeleportData.RestrictedZones.Length > 0)
                            grTeleportData.RestrictedZones += "," + args[0].ToUpper();
                        else
                            grTeleportData.RestrictedZones += args[0].ToUpper();

                        Config.WriteObject(grTeleportData , true);

                        PrintToChat(player , lang.GetMessage("zonesadded" , this , player.UserIDString)
                            .Replace("{zone}" , args[0].ToUpper())
                            .Replace("{zones}" , grTeleportData.RestrictedZones.Replace("ZZZ123,YYY666," , "")));
                    }
                }
                else
                {
                    PrintToChat(player , lang.GetMessage("zonenotadded" , this , player.UserIDString)
                        .Replace("{zones}" , grTeleportData.RestrictedZones.Replace("ZZZ123,YYY666," , "")));
                }
            }
            catch (Exception ex)
            {
                throw new Exception("chmAddZone " + ex.Message);
            }
        }

        [ChatCommand("removezone")]
        void ChmRemoveZone(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "chmRemovezone" , adminPermission))
                    return;

                if (args.Length > 0)
                {
                    if (string.IsNullOrEmpty(grTeleportData.RestrictedZones))
                    {
                        PrintToChat(player , lang.GetMessage("nozones" , this , player.UserIDString));
                        return;
                    }

                    List<string> tmpZoneList = new List<string>(grTeleportData.RestrictedZones.Split(','));

                    if (args[0].Contains(","))
                    {
                        string[] split = args[0].ToUpper().Split(',');
                        for (int i = 0; i < split.Length; i++)
                        {
                            string z = split[i];
                            if (tmpZoneList.Contains(z))
                                tmpZoneList.Remove(z);
                        }

                        grTeleportData.RestrictedZones = string.Join("," , tmpZoneList.ToArray());
                        Config.WriteObject(grTeleportData , true);

                        PrintToChat(player , lang.GetMessage("zonesremoved" , this , player.UserIDString)
                            .Replace("{zone}" , args[0].ToUpper())
                            .Replace("{zones}" , grTeleportData.RestrictedZones));
                    }
                    else
                    {
                        string zone = args[0].ToUpper();
                        if (tmpZoneList.Contains(zone))
                            tmpZoneList.Remove(zone);

                        grTeleportData.RestrictedZones = string.Join("," , tmpZoneList.ToArray());
                        Config.WriteObject(grTeleportData , true);

                        PrintToChat(player , lang.GetMessage("zoneremoved" , this , player.UserIDString)
                            .Replace("{zone}" , args[0].ToUpper())
                            .Replace("{zones}" , grTeleportData.RestrictedZones));
                    }
                }
                else
                {
                    PrintToChat(player , lang.GetMessage("zonenotremoved" , this , player.UserIDString)
                        .Replace("{zones}" , grTeleportData.RestrictedZones));
                }
            }
            catch (Exception ex)
            {
                throw new Exception("chmRemoveZone " + ex.Message);
            }
        }

        [ChatCommand("clearzones")]
        void ChmClearZones(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "chmClearZones" , adminPermission))
                    return;

                List<string> tmpZoneList = new List<string>(grTeleportData.RestrictedZones.Split(','));

                if (string.IsNullOrEmpty(grTeleportData.RestrictedZones) || grTeleportData.RestrictedZones == "ZZZ123,YYY666")
                {
                    PrintToChat(player , lang.GetMessage("addingallzones" , this , player.UserIDString));

                    for (int i = 0; i < spawnGrid.Count; i++)
                        tmpZoneList.Add(spawnGrid[i].GridReference);

                    grTeleportData.RestrictedZones = string.Join("," , tmpZoneList.ToArray());
                    Config.WriteObject(grTeleportData , true);
                    return;
                }

                PrintToChat(player , grTeleportData.RestrictedZones);
                PrintToChat(player , lang.GetMessage("clearingallzones" , this , player.UserIDString));
                grTeleportData.RestrictedZones = "ZZZ123,YYY666";
            }
            catch (Exception ex)
            {
                throw new Exception("chmClearZones " + ex.Message);
            }
        }

        [ChatCommand("togglebuildingblocked")]
        void ChmToggleBuildingBlocked(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "togglebuildingblocked" , adminPermission))
                    return;

                grTeleportData.AllowBuildingBlocked = !grTeleportData.AllowBuildingBlocked;
                Config.WriteObject(grTeleportData , true);

                PrintToChat(player , lang.GetMessage("cuboardreply" , this , player.UserIDString)
                    .Replace("{togglebuildingblocked}" , grTeleportData.AllowBuildingBlocked.ToString()));
            }
            catch (Exception ex)
            {
                throw new Exception("chmToggleBuildingBlocked " + ex.Message);
            }
        }

        [ChatCommand("togglecargoship")]
        void ChmToggleCargoShip(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "togglecargoship" , adminPermission))
                    return;

                grTeleportData.AllowOffCargoship = !grTeleportData.AllowOffCargoship;
                Config.WriteObject(grTeleportData , true);

                PrintToChat(player , lang.GetMessage("cargoshipreply" , this , player.UserIDString)
                    .Replace("{togglecargoship}" , grTeleportData.AllowOffCargoship.ToString()));
            }
            catch (Exception ex)
            {
                throw new Exception("chmToggleCargoShip " + ex.Message);
            }
        }

        [ChatCommand("toggleavoidwater")]
        void ChmSetAvoidWater(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "toggleavoidwater" , adminPermission))
                    return;

                grTeleportData.AvoidWater = !grTeleportData.AvoidWater;
                Config.WriteObject(grTeleportData , true);

                PrintToChat(player , lang.GetMessage("avoidwaterreply" , this , player.UserIDString)
                    .Replace("{avoidwater}" , grTeleportData.AvoidWater.ToString()));
            }
            catch (Exception ex)
            {
                throw new Exception("chmSetAvoidWater " + ex.Message);
            }
        }

        [ChatCommand("setgroup")]
        void ChmSetGroup(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (args.Length == 0)
                {
                    PrintToChat(player , lang.GetMessage("setgroupusage" , this , player.UserIDString));
                    return;
                }

                string groupName = "";
                int cooldownInSeconds = grTeleportData.CooldownInSeconds;
                int dailyLimit = grTeleportData.LimitPerDay;

                if (args.Length == 3)
                {
                    try
                    {
                        groupName = args[0];
                        cooldownInSeconds = int.Parse(args[1]);
                        dailyLimit = int.Parse(args[2]);
                    }
                    catch
                    {
                        PrintToChat(player , lang.GetMessage("setgroupusageerror" , this , player.UserIDString));
                        return;
                    }
                }
                else
                {
                    PrintToChat(player , lang.GetMessage("setgroupusageerror" , this , player.UserIDString));
                    return;
                }

                if (!grTeleportData.groupData.ContainsKey(groupName))
                {
                    grTeleportData.groupData.Add(groupName , new GroupData
                    {
                        coolDownPeriodSeconds = cooldownInSeconds ,
                        dailyTeleports = dailyLimit ,
                        groupName = groupName ,
                        SecondsBeforeTeleport = grTeleportData.SecondsBeforeTeleport ,
                        MinHealthForTeleport = grTeleportData.MinHealthForTeleport ,
                        MinThirstForTeleport = grTeleportData.MinThirstForTeleport ,
                        MinTemperatureForTeleport = grTeleportData.MinTemperatureForTeleport ,
                        MaxTemperatureForTeleport = grTeleportData.MaxTemperatureForTeleport ,
                        MinHungerForTeleport = grTeleportData.MinHungerForTeleport
                    });
                }
                else
                {
                    grTeleportData.groupData[groupName].coolDownPeriodSeconds = cooldownInSeconds;
                    grTeleportData.groupData[groupName].dailyTeleports = dailyLimit;
                }

                Config.WriteObject(grTeleportData , true);

                PrintToChat(player , "Saved " + groupName +
                    " cooldown:" + grTeleportData.groupData[groupName].coolDownPeriodSeconds +
                    " Limit:" + grTeleportData.groupData[groupName].dailyTeleports + ".");
            }
            catch (Exception ex)
            {
                throw new Exception("chmSetGroup " + ex.Message);
            }
        }

        [ChatCommand("getgroups")]
        void ChmGetGroups(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "getgroups" , adminPermission))
                    return;

                if (grTeleportData.groupData.Count < 1)
                {
                    PrintToChat(player , lang.GetMessage("nogroups" , this , player.UserIDString));
                    return;
                }

                foreach (KeyValuePair<string , GroupData> g in grTeleportData.groupData)
                    PrintToChat(player , g.Key + " cooldown:" + g.Value.coolDownPeriodSeconds + " Limit:" + g.Value.dailyTeleports + ".");

                return;
            }
            catch (Exception ex)
            {
                throw new Exception("chmGetGroups " + ex.Message);
            }
        }

        [ChatCommand("clearcooldown")]
        void ChmClearCooldown(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "clearcooldown" , adminPermission))
                    return;

                if (args.Length == 1)
                {
                    Cooldown cd = null;

                    for (int i = 0; i < coolDowns.Count; i++)
                    {
                        if (coolDowns[i].name.ToLower().Contains(args[0].ToLower()))
                        {
                            cd = coolDowns[i];
                            break;
                        }
                    }

                    if (cd != null)
                    {
                        coolDowns.Remove(cd);
                        PrintToChat("Cleared cooldown for " + cd.name + "..");
                    }
                    else
                    {
                        PrintToChat(args[0] + " not found.");
                    }

                    return;
                }

                coolDowns.Clear();
                PrintToChat("Cleared Cooldowns");
            }
            catch (Exception ex)
            {
                throw new Exception("chmClearCooldown " + ex.Message);
            }
        }

        [ChatCommand("clearlimit")]
        void ChmClearLimit(BasePlayer player , string command , string[] args)
        {
            try
            {
                if (!CheckAccess(player , "clearlimit" , adminPermission))
                    return;

                if (args.Length == 1)
                {
                    RustUser ru = null;

                    for (int i = 0; i < rustUsers.Count; i++)
                    {
                        if (rustUsers[i].Player.displayName.ToLower().Contains(args[0].ToLower()))
                        {
                            ru = rustUsers[i];
                            break;
                        }
                    }

                    if (ru != null)
                    {
                        GroupData userGroupData = GetUsersGroupDataOrDefault(ru.Player);
                        RustUser user = GetOrCreateUser(ru.Player , userGroupData);
                        user.TeleportsRemaining = userGroupData.dailyTeleports;
                        PrintToChat("Cleared limit for " + ru.Player.displayName + "..");
                    }
                    else
                    {
                        PrintToChat(args[0] + " not found.");
                    }

                    return;
                }

                rustUsers.Clear();
                PrintToChat("Cleared Limits");
            }
            catch (Exception ex)
            {
                throw new Exception("chmClearLimit " + ex.Message);
            }
        }

        #endregion

        #region API

        private bool TeleportToGridReference(BasePlayer player , string gridReference , bool avoidWater = true)
        {
            try
            {
                int index = GridIndexFromReference(gridReference);

                if (index == -1)
                {
                    player.ChatMessage(lang.GetMessage("CannotGridTeleport" , this , player.UserIDString));
                    return false;
                }

                SpawnPosition sp = spawnGrid[index];

                Vector3 destination = FindValidPositionInGridCell(player , sp , true , true);

                if (destination == Vector3.zero)
                {
                    player.ChatMessage(lang.GetMessage("CannotGridTeleport" , this , player.UserIDString));
                    return false;
                }

                sp.GroundPosition = destination;
                sp.LastTeleportPosition = destination;

                player.SetParent(null , 0);
                player.EnsureDismounted();
                player.Teleport(destination);

                return true;
            }
            catch (Exception ex)
            {
                throw new Exception("TeleportToGridReference " + ex.Message);
            }
        }

        private bool IsGridReferenceAboveWater(string gridReference)
        {
            try
            {
                int index = GridIndexFromReference(gridReference);
                if (index < 0 || index >= spawnGrid.Count)
                    return false;

                SpawnPosition sp = spawnGrid[index];
                Vector3 destination = FindValidPositionInGridCell(null , sp , false , true);

                return destination != Vector3.zero;
            }
            catch (Exception ex)
            {
                throw new Exception("IsGridReferenceAboveWater " + ex.Message);
            }
        }

        #endregion

        #region supporting functions

        private int GetGridCount()
        {
            return Mathf.CeilToInt(ConVar.Server.worldsize / DefaultGridCellSize);
        }

        private float GetCellSize()
        {
            int gridCount = GetGridCount();
            return ConVar.Server.worldsize / gridCount;
        }

        private string ColumnToLetters(int columnIndex)
        {
            columnIndex++;

            string result = string.Empty;

            while (columnIndex > 0)
            {
                columnIndex--;
                result = (char)('A' + (columnIndex % 26)) + result;
                columnIndex /= 26;
            }

            return result;
        }

        private bool TryLettersToColumn(string letters , out int columnIndex)
        {
            columnIndex = 0;

            if (string.IsNullOrWhiteSpace(letters))
                return false;

            letters = letters.Trim().ToUpperInvariant();

            for (int i = 0; i < letters.Length; i++)
            {
                char c = letters[i];

                if (c < 'A' || c > 'Z')
                    return false;

                columnIndex = (columnIndex * 26) + (c - 'A' + 1);
            }

            columnIndex--;
            return true;
        }

        private bool TryParseGridReference(string gridReference , out int col , out int row)
        {
            col = -1;
            row = -1;

            if (string.IsNullOrWhiteSpace(gridReference))
                return false;

            gridReference = gridReference.Trim().ToUpperInvariant();

            int splitIndex = 0;
            while (splitIndex < gridReference.Length && char.IsLetter(gridReference[splitIndex]))
                splitIndex++;

            if (splitIndex == 0 || splitIndex >= gridReference.Length)
                return false;

            string letters = gridReference.Substring(0 , splitIndex);
            string numbers = gridReference.Substring(splitIndex);

            if (!TryLettersToColumn(letters , out col))
                return false;

            if (!int.TryParse(numbers , out row))
                return false;

            int gridCount = GetGridCount();
            if (col < 0 || col >= gridCount || row < 0 || row >= gridCount)
                return false;

            return true;
        }

        string GetGridReference(Vector3 position)
        {
            try
            {
                float worldSize = ConVar.Server.worldsize;
                float half = worldSize * 0.5f;
                float cellSize = GetCellSize();
                int gridCount = GetGridCount();

                int col = Mathf.FloorToInt((position.x + half) / cellSize);
                int row = Mathf.FloorToInt((half - position.z) / cellSize);

                col = Mathf.Clamp(col , 0 , gridCount - 1);
                row = Mathf.Clamp(row , 0 , gridCount - 1);

                string gridRef = ColumnToLetters(col) + row;

                float centerX = -half + (col * cellSize) + (cellSize * 0.5f);
                float centerZ = half - (row * cellSize) - (cellSize * 0.5f);
                Vector3 center = new Vector3(centerX , position.y , centerZ);

                float distance = Vector3.Distance(
                    new Vector3(position.x , 0f , position.z) ,
                    new Vector3(center.x , 0f , center.z));

                string direction = string.Empty;

                if (position.z > center.z)
                    direction += "N";
                else if (position.z < center.z)
                    direction += "S";

                if (position.x > center.x)
                    direction += "E";
                else if (position.x < center.x)
                    direction += "W";

                return distance.ToString("0.00") + " meters " + direction + " of the " + gridRef + " map marker.";
            }
            catch (Exception ex)
            {
                throw new Exception("GetGridReference " + ex.Message);
            }
        }

        List<SpawnPosition> CreateSpawnGrid()
        {
            try
            {
                List<SpawnPosition> result = new List<SpawnPosition>();

                float worldSize = ConVar.Server.worldsize;
                float half = worldSize * 0.5f;

                int gridCount = Mathf.CeilToInt(worldSize / DefaultGridCellSize);
                float cellSize = worldSize / gridCount;

                for (int row = 0; row < gridCount; row++)
                {
                    float maxZ = half - (row * cellSize);
                    float minZ = maxZ - cellSize;

                    for (int col = 0; col < gridCount; col++)
                    {
                        float minX = -half + (col * cellSize);
                        float maxX = minX + cellSize;

                        string gridRef = ColumnToLetters(col) + row;
                        SpawnPosition sp = new SpawnPosition(gridRef , minX , maxX , minZ , maxZ);
                        sp.GroundPosition = FindValidPositionInGridCell(null , sp , false , true);

                        result.Add(sp);
                    }
                }

                return result;
            }
            catch (Exception ex)
            {
                throw new Exception("CreateSpawnGrid " + ex.Message);
            }
        }

        List<Vector3> BuildCandidatePoints(SpawnPosition sp)
        {
            List<Vector3> points = new List<Vector3>();

            float width = sp.MaxX - sp.MinX;
            float height = sp.MaxZ - sp.MinZ;

            float x20 = sp.MinX + width * 0.2f;
            float x35 = sp.MinX + width * 0.35f;
            float x65 = sp.MinX + width * 0.65f;
            float x80 = sp.MinX + width * 0.8f;

            float z20 = sp.MinZ + height * 0.2f;
            float z35 = sp.MinZ + height * 0.35f;
            float z65 = sp.MinZ + height * 0.65f;
            float z80 = sp.MinZ + height * 0.8f;

            points.Add(new Vector3(x20 , 0 , z20));
            points.Add(new Vector3(x20 , 0 , z80));
            points.Add(new Vector3(x80 , 0 , z20));
            points.Add(new Vector3(x80 , 0 , z80));

            points.Add(new Vector3(x35 , 0 , z35));
            points.Add(new Vector3(x35 , 0 , z65));
            points.Add(new Vector3(x65 , 0 , z35));
            points.Add(new Vector3(x65 , 0 , z65));

            return points;
        }

        void TryAddValidPosition(List<Vector3> validPositions , BasePlayer player , Vector3 candidate , bool checkBuildingBlocked , bool avoidWater)
        {
            for (int attempt = 0; attempt < 30; attempt++)
            {
                Vector3 tryPos = candidate;

                if (attempt > 0)
                {
                    Vector2 circle = UnityEngine.Random.insideUnitCircle * 100;
                    tryPos = new Vector3(candidate.x + circle.x , 0f , candidate.z + circle.y);
                }

                NavMeshHit navHit;
                if (!NavMesh.SamplePosition(new Vector3(tryPos.x , 0f , tryPos.z) , out navHit , 20f , NavMesh.AllAreas))
                    continue;

                Vector3 ground = navHit.position + new Vector3(0f , AboveGroundPosition , 0f);
                Vector3 feet = navHit.position;

                if (!IsGroundCloseEnough(tryPos , ground , 10f))
                    continue;

                if (avoidWater && grTeleportData.AvoidWater)
                {
                    if (feet.y < 0f)
                        continue;

                    if (ground.y < AboveGroundPosition)
                        continue;

                    Vector3[] checks =
                    {
                feet,
                feet + new Vector3(1.5f, 0f, 0f),
                feet + new Vector3(-1.5f, 0f, 0f),
                feet + new Vector3(0f, 0f, 1.5f),
                feet + new Vector3(0f, 0f, -1.5f),
                feet + new Vector3(1.1f, 0f, 1.1f),
                feet + new Vector3(-1.1f, 0f, 1.1f),
                feet + new Vector3(1.1f, 0f, -1.1f),
                feet + new Vector3(-1.1f, 0f, -1.1f)
            };

                    bool wet = false;

                    for (int i = 0; i < checks.Length; i++)
                    {
                        Vector3 check = checks[i];

                        float terrainY = TerrainMeta.HeightMap.GetHeight(check);
                        float waterY = TerrainMeta.WaterMap.GetHeight(check);

                        if (terrainY < 0f)
                        {
                            wet = true;
                            break;
                        }

                        if (check.y < 0f)
                        {
                            wet = true;
                            break;
                        }

                        if (waterY > terrainY + 0.01f)
                        {
                            wet = true;
                            break;
                        }

                        if (Physics.CheckSphere(check + new Vector3(0f , 0.5f , 0f) , 1.0f , Rust.Layers.Water))
                        {
                            wet = true;
                            break;
                        }

                        RaycastHit waterHit;
                        if (Physics.Raycast(check + new Vector3(0f , 2f , 0f) , Vector3.down , out waterHit , 8f , Rust.Layers.Water))
                        {
                            wet = true;
                            break;
                        }
                    }

                    if (wet)
                        continue;
                }

                if (checkBuildingBlocked && !grTeleportData.AllowBuildingBlocked)
                {
                    if (player != null && player.IsBuildingBlocked(ground , Quaternion.identity , new Bounds(Vector3.zero , Vector3.zero)))
                        continue;
                }

                if (ContainsNearPosition(validPositions , ground , 3f))
                    continue;

                validPositions.Add(ground);
                return;
            }
        }

        bool ContainsNearPosition(List<Vector3> positions , Vector3 target , float maxDistance)
        {
            for (int i = 0; i < positions.Count; i++)
            {
                if (Vector3.Distance(positions[i] , target) <= maxDistance)
                    return true;
            }

            return false;
        }

        Vector3 FindValidPositionInGridCell(BasePlayer player , SpawnPosition sp , bool checkBuildingBlocked , bool avoidWater)
        {
            List<Vector3> validPositions = new List<Vector3>();
            List<Vector3> candidates = BuildCandidatePoints(sp);

            for (int i = 0; i < candidates.Count; i++)
            {
                TryAddValidPosition(validPositions , player , candidates[i] , checkBuildingBlocked , avoidWater);
            }

            for (int i = 0; i < 60; i++)
            {
                float x = UnityEngine.Random.Range(sp.MinX + 2f , sp.MaxX - 2f);
                float z = UnityEngine.Random.Range(sp.MinZ + 2f , sp.MaxZ - 2f);

                Vector3 candidate = new Vector3(x , 0f , z);

                TryAddValidPosition(validPositions , player , candidate , checkBuildingBlocked , avoidWater);
            }

            if (validPositions.Count == 0)
                return Vector3.zero;

            if (validPositions.Count == 1)
                return validPositions[0];

            List<Vector3> filtered = new List<Vector3>();

            for (int i = 0; i < validPositions.Count; i++)
            {
                if (sp.LastTeleportPosition == Vector3.zero ||
                    Vector3.Distance(validPositions[i] , sp.LastTeleportPosition) > 10f)
                {
                    filtered.Add(validPositions[i]);
                }
            }

            Vector3 chosen;

            if (filtered.Count > 0)
                chosen = filtered[UnityEngine.Random.Range(0 , filtered.Count)];
            else
                chosen = validPositions[UnityEngine.Random.Range(0 , validPositions.Count)];

            return chosen;
        }

        private string CanPlayerTeleport(BasePlayer player)
        {
            return Interface.Oxide.CallHook("CanTeleport" , player) as string;
        }

        bool TestCanGrtTeleport(BasePlayer player , string gr)
        {
            string err = CanPlayerTeleport(player);
            if (err != null)
            {
                PrintToChat(player , err);
                return false;
            }

            if (player.IsAdmin)
                return true;

            GroupData userGroupData = GetUsersGroupDataOrDefault(player);
            int index = GridIndexFromReference(gr);
            string parent = string.Empty;

            try
            {
                BaseEntity parentEntity = player.GetParentEntity();
                parent = parentEntity != null ? parentEntity.ToString() : string.Empty;
            }
            catch
            {
                parent = string.Empty;
            }

            if (index == -1)
            {
                PrintToChat(player , lang.GetMessage("CannotGridTeleport" , this , player.UserIDString));
                return false;
            }

            if (!grTeleportData.AllowBuildingBlocked)
            {
                if (player.IsBuildingBlocked(player.transform.position , Quaternion.identity , new Bounds(Vector3.zero , Vector3.zero)))
                {
                    PrintToChat(player , lang.GetMessage("buildingblocked" , this , player.UserIDString));
                    return false;
                }
            }

            if (parent.Contains("cargoship") && !grTeleportData.AllowOffCargoship)
            {
                PrintToChat(player , lang.GetMessage("cargoship" , this , player.UserIDString));
                return false;
            }

            if (userGroupData == null)
            {
                PrintToChat(player , lang.GetMessage("noaccess" , this , player.UserIDString));
                return false;
            }

            if (player.metabolism.calories.value <= userGroupData.MinHungerForTeleport)
            {
                PrintToChat(player , lang.GetMessage("hungrymessage" , this , player.UserIDString));
                return false;
            }

            if (player.metabolism.hydration.value <= userGroupData.MinThirstForTeleport)
            {
                PrintToChat(player , lang.GetMessage("thirstymessage" , this , player.UserIDString));
                return false;
            }

            if (player.metabolism.temperature.value <= userGroupData.MinTemperatureForTeleport)
            {
                PrintToChat(player , lang.GetMessage("coldmessage" , this , player.UserIDString)
                    .Replace("{temperature}" , player.metabolism.temperature.value.ToString()));
                return false;
            }

            if (player.metabolism.temperature.value >= userGroupData.MaxTemperatureForTeleport)
            {
                PrintToChat(player , lang.GetMessage("hotmessage" , this , player.UserIDString)
                    .Replace("{toohot}" , userGroupData.MaxTemperatureForTeleport.ToString())
                    .Replace("{temperature}" , player.metabolism.temperature.value.ToString()));
                return false;
            }

            if (player.metabolism.radiation_poison.value > 0)
            {
                PrintToChat(player , lang.GetMessage("radiatedmessage" , this , player.UserIDString));
                return false;
            }

            if (player.IsWounded())
            {
                PrintToChat(player , lang.GetMessage("woundedmessage" , this , player.UserIDString));
                return false;
            }

            return true;
        }

        int GridIndexFromReference(string gridReference)
        {
            try
            {
                int col;
                int row;

                if (!TryParseGridReference(gridReference , out col , out row))
                    return -1;

                int gridCount = GetGridCount();
                int index = (row * gridCount) + col;

                if (index < 0 || index >= spawnGrid.Count)
                    return -1;

                return index;
            }
            catch (Exception ex)
            {
                throw new Exception("GridIndexFromReference " + ex.Message);
            }
        }

        bool TestRestrictedZone(string gridReference)
        {
            if (string.IsNullOrEmpty(grTeleportData.RestrictedZones))
                return false;

            if (grTeleportData.RestrictedZones.Contains(","))
            {
                string[] zones = grTeleportData.RestrictedZones.Split(',');
                for (int i = 0; i < zones.Length; i++)
                {
                    if (zones[i].ToUpper() == gridReference.ToUpper())
                        return true;
                }
            }
            else
            {
                if (grTeleportData.RestrictedZones.ToUpper() == gridReference.ToUpper())
                    return true;
            }

            return false;
        }

        Cooldown GetCooldown(string playerName)
        {
            try
            {
                for (int i = coolDowns.Count - 1; i >= 0; i--)
                {
                    if (coolDowns[i].expirtyDateTime <= DateTime.Now)
                        coolDowns.RemoveAt(i);
                }

                for (int i = 0; i < coolDowns.Count; i++)
                {
                    if (coolDowns[i].name.ToLower() == playerName.ToLower())
                        return coolDowns[i];
                }

                return null;
            }
            catch (Exception ex)
            {
                throw new Exception("GetCooldown " + ex.Message , ex);
            }
        }

        bool CheckAccess(BasePlayer player , string command , string sPermission , bool onErrorDisplayMessageToUser = true)
        {
            try
            {
                if (!permission.UserHasPermission(player.UserIDString , sPermission))
                {
                    if (onErrorDisplayMessageToUser)
                        PrintToChat(player , lang.GetMessage("noaccess" , this , player.UserIDString));

                    return false;
                }

                return true;
            }
            catch (Exception ex)
            {
                throw new Exception("CheckAccess " + ex.Message);
            }
        }

        void AddToCoolDown(string userName , int seconds)
        {
            coolDowns.Add(new Cooldown(userName.ToLower() , seconds));
        }

        RustUser GetOrCreateUser(BasePlayer player , GroupData groupData)
        {
            int index = -1;

            for (int i = 0; i < rustUsers.Count; i++)
            {
                if (rustUsers[i].Player == player)
                {
                    index = i;
                    break;
                }
            }

            if (index == -1)
            {
                RustUser user = new RustUser
                {
                    Player = player ,
                    TeleportsRemaining = groupData.dailyTeleports ,
                    ResetDateTime = DateTime.Now.AddHours(24) ,
                    CoolDownInSeconds = groupData.coolDownPeriodSeconds ,
                    isWaitingForTeleport = false
                };

                rustUsers.Add(user);
                return user;
            }

            if (rustUsers[index].ResetDateTime <= DateTime.Now)
            {
                rustUsers[index].ResetDateTime = DateTime.Now.AddHours(24);
                rustUsers[index].TeleportsRemaining = groupData.dailyTeleports;
                rustUsers[index].CoolDownInSeconds = groupData.coolDownPeriodSeconds;
            }

            return rustUsers[index];
        }

        Vector3 GetActualGroundPosition(Vector3 sourcePos)
        {
            RaycastHit hitInfo;

            if (Physics.Raycast(sourcePos , Vector3.down , out hitInfo))
                sourcePos.y = hitInfo.point.y;

            sourcePos.y = Mathf.Max(sourcePos.y , TerrainMeta.HeightMap.GetHeight(sourcePos));

            return sourcePos;
        }

        GroupData GetUsersGroupDataOrDefault(BasePlayer user)
        {
            if (permission.UserHasPermission(user.UserIDString , adminPermission))
            {
                GroupData groupData = new GroupData
                {
                    coolDownPeriodSeconds = 0 ,
                    dailyTeleports = 9999 ,
                    groupName = "admin" ,
                    SecondsBeforeTeleport = grTeleportData.SecondsBeforeTeleport ,
                    MinHealthForTeleport = grTeleportData.MinHealthForTeleport ,
                    MinThirstForTeleport = grTeleportData.MinThirstForTeleport ,
                    MinTemperatureForTeleport = grTeleportData.MinTemperatureForTeleport ,
                    MaxTemperatureForTeleport = grTeleportData.MaxTemperatureForTeleport ,
                    MinHungerForTeleport = grTeleportData.MinHungerForTeleport
                };

                return groupData;
            }

            foreach (KeyValuePair<string , GroupData> p in grTeleportData.groupData)
            {
                string groupName = p.Key;
                GroupData groupData = p.Value;

                if (permission.UserHasGroup(user.UserIDString , groupName))
                    return groupData;
            }

            if (permission.UserHasPermission(user.UserIDString , grtPermission))
            {
                GroupData groupData = new GroupData
                {
                    coolDownPeriodSeconds = grTeleportData.CooldownInSeconds ,
                    dailyTeleports = grTeleportData.LimitPerDay ,
                    groupName = "default" ,
                    SecondsBeforeTeleport = grTeleportData.SecondsBeforeTeleport ,
                    MinHealthForTeleport = grTeleportData.MinHealthForTeleport ,
                    MinThirstForTeleport = grTeleportData.MinThirstForTeleport ,
                    MinTemperatureForTeleport = grTeleportData.MinTemperatureForTeleport ,
                    MaxTemperatureForTeleport = grTeleportData.MaxTemperatureForTeleport ,
                    MinHungerForTeleport = grTeleportData.MinHungerForTeleport
                };

                return groupData;
            }

            return null;
        }

        #endregion
    }
}