﻿using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Configuration;
using Newtonsoft.Json.Converters;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Spawns", "Reneb / k1lly0u", "2.0.36"), Description("A database of sets of spawn points, created by a user and used by other plugins")]
    class Spawns : RustPlugin
    {
        #region Fields
        private SpawnsData _spawnsData;

        private Dictionary<string, List<Vector3>> _loadedSpawnfiles = new Dictionary<string, List<Vector3>>();

        private Dictionary<ulong, List<Vector3>> _spawnFileCreators = new Dictionary<ulong, List<Vector3>>();

        private List<ulong> _isEditing = new List<ulong>();
        #endregion

        #region Oxide Hooks
        private void Loaded() => LoadData();
        
        protected override void LoadDefaultMessages() => lang.RegisterMessages(Messages, this);

        private void OnServerInitialized() => VerifyFilesExist();
        #endregion

        #region Functions
        private void VerifyFilesExist()
        {
            bool hasChanged = false;
            for (int i = 0; i < _spawnsData.Spawnfiles.Count; i++)
            {
                string name = _spawnsData.Spawnfiles[i];

                if (!Interface.Oxide.DataFileSystem.ExistsDatafile($"SpawnsDatabase/{name}"))
                {
                    _spawnsData.Spawnfiles.Remove(name);
                    hasChanged = true;
                } 
                else 
                {                    
                    if (LoadSpawns(name) != null)
                    {
                        _spawnsData.Spawnfiles.Remove(name);
                        hasChanged = true;
                    }
                    else if (_loadedSpawnfiles[name].Count == 0)
                    {
                        _spawnsData.Spawnfiles.Remove(name);
                        hasChanged = true;
                    }
                }               
            }

            if (hasChanged)
                SaveData();
        }
        
        private object LoadSpawns(string name)
        {
            if (string.IsNullOrEmpty(name))
                return Message("noFile");

            if (!_loadedSpawnfiles.ContainsKey(name))
            {                
                object success = LoadSpawnFile(name);
                if (success == null)                
                    return Message("noFile");                
                else _loadedSpawnfiles.Add(name, (List<Vector3>)success);
            }
            return null;
        }
        #endregion

        #region API
        private object GetSpawnsCount(string filename)
        {
            object success = LoadSpawns(filename);
            if (success != null)
                return (string)success;

            return _loadedSpawnfiles[filename].Count;
        }

        private object GetRandomSpawn(string filename)
        {
            object success = LoadSpawns(filename);
            if (success != null)
                return (string)success;

            return _loadedSpawnfiles[filename].GetRandom();
        }

        private object GetRandomSpawnRange(string filename, int min, int max)
        {
            object success = LoadSpawns(filename);
            if (success != null)
                return (string)success;

            List<Vector3> list = _loadedSpawnfiles[filename];

            return list[UnityEngine.Random.Range(Mathf.Clamp(min, 0, list.Count - 1), Mathf.Clamp(max, 0, list.Count - 1))];
        }

        private object GetSpawn(string filename, int number)
        {
            object success = LoadSpawns(filename);
            if (success != null)
                return (string)success;

            List<Vector3> list = _loadedSpawnfiles[filename];

            return list[Mathf.Clamp(number, 0, list.Count - 1)];
        }

        private string[] GetSpawnfileNames() => _spawnsData.Spawnfiles.ToArray();
        #endregion

        #region Chat Commands
        [ChatCommand("spawns")]
        void cmdSpawns(BasePlayer player, string command, string[] args)
        {
            if (player.net.connection.authLevel < 1)
            {
                SendReply(player, Message("noAccess", player.UserIDString));
                return;
            }
           
            if (args == null || args.Length == 0)
            {
                SendHelpText(player);
                return;
            }

            if (args.Length >= 1)
            {
                switch (args[0].ToLower())
                {
                    case "new":
                        if (IsCreatingFile(player))
                        {
                            SendReply(player, Message("alreadyCreating", player.UserIDString));
                            return;
                        }

                        _spawnFileCreators.Add(player.userID, new List<Vector3>());

                        SendReply(player, Message("newCreating", player.UserIDString));
                        return;

                    case "open":
                        if (args.Length >= 2)
                        {
                            if (IsCreatingFile(player))
                            {
                                SendReply(player, Message("isCreating", player.UserIDString));
                                return;
                            }
                            object spawns = LoadSpawnFile(args[1]);
                            if (spawns != null)
                            {
                                _spawnFileCreators.Add(player.userID, (List<Vector3>)spawns);
                                SendReply(player, string.Format(Message("opened", player.UserIDString), _spawnFileCreators[player.userID].Count));
                                _isEditing.Add(player.userID);
                            }
                            else SendReply(player, Message("invalidFile", player.UserIDString));                            
                        }
                        else SendReply(player, Message("fileName", player.UserIDString));
                        return;

                    case "add":
                        if (!IsCreatingFile(player))
                        {
                            SendReply(player, Message("notCreating", player.UserIDString));
                            return;
                        }
                        else
                        {                            
                            _spawnFileCreators[player.userID].Add(player.transform.position);
                            int number = _spawnFileCreators[player.userID].Count;
                            DDrawPosition(player, _spawnFileCreators[player.userID][number - 1], number.ToString());
                            SendReply(player, string.Format("Added Spawn n°{0}", _spawnFileCreators[player.userID].Count));
                        }
                        return;

                    case "remove":
                        if (args.Length >= 2)
                        {
                            if (!IsCreatingFile(player))
                            {
                                SendReply(player, Message("notCreating", player.UserIDString));
                                return;
                            }

                            if (_spawnFileCreators[player.userID].Count > 0)
                            {
                                int number;
                                if (int.TryParse(args[1], out number))
                                {
                                    if (number <= _spawnFileCreators[player.userID].Count)
                                    {
                                        _spawnFileCreators[player.userID].RemoveAt(number - 1);
                                        SendReply(player, string.Format(Message("remSuccess", player.UserIDString), number));
                                    }
                                    else SendReply(player, Message("nexistNum", player.UserIDString));
                                }
                                else SendReply(player, Message("noNum", player.UserIDString));
                            }
                            else SendReply(player, Message("noSpawnpoints", player.UserIDString));
                        }
                        else SendReply(player, "/spawns remove <number>");
                        return;

                    case "save":
                        if (args.Length >= 2)
                        {
                            if (!IsCreatingFile(player))
                            {
                                SendReply(player, Message("noCreate", player.UserIDString));
                                return;
                            }
                            if (_spawnFileCreators.ContainsKey(player.userID) && _spawnFileCreators[player.userID].Count > 0)
                            {
                                if (!_spawnsData.Spawnfiles.Contains(args[1]) && !_loadedSpawnfiles.ContainsKey(args[1]))
                                {
                                    SendReply(player, string.Format(Message("saved", player.UserIDString), _spawnFileCreators[player.userID].Count, args[1]));
                                    SaveSpawnFile(player, args[1]);
                                    return;                                    
                                }

                                if (_isEditing.Contains(player.userID))
                                {
                                    SaveSpawnFile(player, args[1]);
                                    SendReply(player, string.Format(Message("overwriteSuccess", player.UserIDString), args[1]));
                                    _isEditing.Remove(player.userID);
                                    return;
                                }

                                SendReply(player, Message("spawnfileExists", player.UserIDString));
                                return;
                            }
                            else SendReply(player, Message("noSpawnpoints", player.UserIDString));
                        }
                        else SendReply(player, "/spawns save <filename>");
                        return;

                    case "close":
                        if (!IsCreatingFile(player))
                        {
                            SendReply(player, Message("noCreate", player.UserIDString));
                            return;
                        }
                        _spawnFileCreators.Remove(player.userID);
                        SendReply(player, Message("noSave", player.UserIDString));
                        return;

                    case "show":
                        if (!IsCreatingFile(player))
                        {
                            SendReply(player, Message("notCreating", player.UserIDString));
                            return;
                        }
                        if (_spawnFileCreators[player.userID].Count > 0)
                        {
                            float time = 10f;
                            if (args.Length > 1)
                                float.TryParse(args[1], out time);

                            for (int i = 0; i < _spawnFileCreators[player.userID].Count; i++)                            
                                DDrawPosition(player, _spawnFileCreators[player.userID][i], i.ToString(), time);
                            
                            return;
                        }
                        else SendReply(player, Message("noSp", player.UserIDString));
                        return;

                    default:
                        SendHelpText(player);
                        break;
                }
            }
        }

        private void DDrawPosition(BasePlayer player, Vector3 point, string name, float time = 10f)
        {
            player.SendConsoleCommand("ddraw.text", time, Color.green, point + new Vector3(0, 1.5f, 0), $"<size=40>{name}</size>");
            player.SendConsoleCommand("ddraw.box", time, Color.green, point, 1f);
        }

        private void SendHelpText(BasePlayer player)
        {
            SendReply(player, Message("newSyn", player.UserIDString));
            SendReply(player, Message("openSyn", player.UserIDString));
            SendReply(player, Message("addSyn", player.UserIDString));
            SendReply(player, Message("remSyn", player.UserIDString));
            SendReply(player, Message("saveSyn", player.UserIDString));
            SendReply(player, Message("closeSyn", player.UserIDString));
            SendReply(player, Message("showSyn", player.UserIDString));
        }

        private bool IsCreatingFile(BasePlayer player) => _spawnFileCreators.ContainsKey(player.userID);
        #endregion

        #region Data Management
        private DynamicConfigFile data;

        private void SaveData() => data.WriteObject(_spawnsData);

        private void LoadData()
        {
            data = Interface.Oxide.DataFileSystem.GetFile("SpawnsDatabase/spawns_data");

            try
            {
                _spawnsData = data.ReadObject<SpawnsData>();
            }
            catch
            {
                _spawnsData = new SpawnsData();
            }
        }

        private void SaveSpawnFile(BasePlayer player, string name)
        {
            DynamicConfigFile configFile = Interface.Oxide.DataFileSystem.GetFile($"SpawnsDatabase/{name}");
            configFile.Clear();
            configFile.Settings.Converters = new JsonConverter[] { new StringEnumConverter(), new UnityVector3Converter() };

            Spawnfile spawnFile = new Spawnfile();

            for (int i1 = 0; i1 < _spawnFileCreators[player.userID].Count; i1++)
            {
                Vector3 spawnpoint = _spawnFileCreators[player.userID][i1];

                spawnFile.spawnPoints.Add(i1.ToString(), spawnpoint);
            }

            configFile.WriteObject(spawnFile);

            if (!_spawnsData.Spawnfiles.Contains(name))
                _spawnsData.Spawnfiles.Add(name);

            if (!_loadedSpawnfiles.ContainsKey(name))
                _loadedSpawnfiles.Add(name, _spawnFileCreators[player.userID]);
            else _loadedSpawnfiles[name] = _spawnFileCreators[player.userID];

            SaveData();
            
            _spawnFileCreators.Remove(player.userID);
        }

        private object LoadSpawnFile(string name)
        {
            if (!Interface.Oxide.DataFileSystem.ExistsDatafile($"SpawnsDatabase/{name}"))
                return null;

            DynamicConfigFile configFile = Interface.GetMod().DataFileSystem.GetDatafile($"SpawnsDatabase/{name}");
            configFile.Settings.Converters = new JsonConverter[] { new StringEnumConverter(), new UnityVector3Converter() };

            Spawnfile spawnFile = new Spawnfile();
            spawnFile = configFile.ReadObject<Spawnfile>();

            List<Vector3> list = spawnFile.spawnPoints.Values.ToList();
            if (list.Count < 1)
                return null;

            return list;
        }

        private class SpawnsData
        {
            public List<string> Spawnfiles = new List<string>();
        }

        private class Spawnfile
        {
            public Dictionary<string, Vector3> spawnPoints = new Dictionary<string, Vector3>();
        }

        private class UnityVector3Converter : JsonConverter
        {
            public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
            {
                Vector3 vector = (Vector3)value;
                writer.WriteValue($"{vector.x} {vector.y} {vector.z}");
            }

            public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
            {
                if (reader.TokenType == JsonToken.String)
                {
                    string[] values = reader.Value.ToString().Trim().Split(' ');
                    return new Vector3(Convert.ToSingle(values[0]), Convert.ToSingle(values[1]), Convert.ToSingle(values[2]));
                }
                JObject o = JObject.Load(reader);
                return new Vector3(Convert.ToSingle(o["x"]), Convert.ToSingle(o["y"]), Convert.ToSingle(o["z"]));
            }

            public override bool CanConvert(Type objectType)
            {
                return objectType == typeof(Vector3);
            }
        }
        #endregion

        #region Messaging
        private string Message(string key, string ID = null) => lang.GetMessage(key, this, ID);

        private readonly Dictionary<string, string> Messages = new Dictionary<string, string>
        {
            {"noFile", "This file doesn't exist" },
            {"alreadyCreating", "You are already creating a spawn file" },
            {"newCreating", "You now creating a new spawn file" },
            {"isCreating", "You must save/close your current spawn file first. Type /spawns for more information" },
            {"opened", "Opened spawnfile with {0} spawns" },
            {"invalidFile", "This spawnfile is empty or not valid" },
            {"fileName", "You must enter a filename" },
            {"notCreating", "You must create/open a new Spawn file first /spawns for more information" },
            {"remSuccess", "Successfully removed spawn n°{0}" },
            {"nexistNum", "This spawn number doesn't exist" },
            {"noNum", "You must enter a spawn point number" },
            {"noSpawnpoints", "You haven't set any spawn points yet" },
            {"noCreate", "You must create a new Spawn file first. Type /spawns for more information" },
            {"noSave", "Spawn file closed without saving" },
            {"noSp", "You must add spawnpoints first" },
            {"newSyn", "/spawns new - Create a new spawn file" },
            {"openSyn", "/spawns open - Open a existing spawn file for editing" },
            {"addSyn", "/spawns add - Add a new spawn point" },
            {"remSyn", "/spawns remove <number> - Remove a spawn point" },
            {"saveSyn", "/spawns save <filename> - Saves your spawn file" },
            {"closeSyn", "/spawns close - Cancel spawn file creation" },
            {"showSyn", "/spawns show <opt:time> - Display a box at each spawnpoint" },
            {"noAccess", "You are not allowed to use this command" },
            {"saved", "{0} spawnpoints saved into {1}" },
            {"spawnfileExists", "A spawn file with that name already exists" },
            {"overwriteSuccess", "You have successfully edited the spawnfile {0}" }
        };
        #endregion
    }
}
