﻿/***********************************************************************************************************************/
/*** DO NOT edit this file! Edit the files under `oxide/config` and/or `oxide/lang`, created once plugin has loaded. ***/
/*** Please note, support cannot be provided if the plugin has been modified. Please use a fresh copy if modified.   ***/
/***********************************************************************************************************************/

//#define DEBUG

using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.IO;
using System.Linq;
using MySql.Data.MySqlClient;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Libraries.Covalence;

namespace Oxide.Plugins
{
    [Info("Economics", "Wulf", "4.0.0")]
    [Description("Basic economics system and economy API")]
    public class Economics : CovalencePlugin
    {
        #region Configuration

        private Configuration _config;

        public class MySqlConfig
        {
            public string Server = "localhost";
            public string Username = "root";
            public string Password = "";
            public string Database = "economics";
        }

        public class Configuration
        {
            [JsonProperty("Balance limit for accounts (0.0 to disable)")]
            public double BalanceLimit = 0.0;

            [JsonProperty("Balance limit for accounts (0 to disable)")]
            private int BalanceLimitOld { set { BalanceLimit = Convert.ToDouble(value); } } // TODO: From version 3.9.1; remove eventually

            [JsonProperty("Negative balance limit for accounts (0.0 to disable)")]
            public double NegativeBalanceLimit;

            [JsonProperty("Negative balance limit for accounts (0 to disable)")]
            public int NegativeBalanceLimitOld { set { NegativeBalanceLimit = Convert.ToDouble(value); } } // TODO: From version 3.9.1; remove eventually

            [JsonProperty("Remove unused accounts")]
            public bool RemoveUnused = true;

            [JsonProperty("Log transactions to file")]
            public bool LogTransactions = false;

            [JsonProperty("Starting account balance (0.0 or higher)")]
            public double StartingBalance = 1000.0;

            [JsonProperty("Starting account balance (0 or higher)")]
            private int StartingBalanceOld { set { StartingBalance = Convert.ToDouble(value); } } // TODO: From version 3.9.1; remove eventually

            [JsonProperty("Wipe balances on new save file")]
            public bool WipeOnNewSave = false;

            [JsonProperty("Store data in SQLite instead of data files")]
            public bool UseSqLite = false;

            [JsonProperty("Store data in MySQL instead of data files")]
            public bool UseMySql = false;

            [JsonProperty("MySQL configuration, if using MySQL")]
            public MySqlConfig MySql = new MySqlConfig();

            public string ToJson() => JsonConvert.SerializeObject(this);

            public Dictionary<string, object> ToDictionary() => JsonConvert.DeserializeObject<Dictionary<string, object>>(ToJson());
        }

        protected override void LoadDefaultConfig() => _config = new Configuration();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Configuration>();
                if (_config == null)
                {
                    throw new JsonException();
                }

                if (!_config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys))
                {
                    LogWarning("Configuration appears to be outdated; updating and saving");
                    SaveConfig();
                }
            }
            catch
            {
                LogWarning($"Configuration file {Name}.json is invalid; using defaults");
                LoadDefaultConfig();
            }
        }

        protected override void SaveConfig()
        {
            LogWarning($"Configuration changes saved to {Interface.Oxide.ConfigDirectory}{Path.DirectorySeparatorChar}{Name}.json");
            Config.WriteObject(_config, true);
        }

        #endregion Configuration

        #region Stored Data

        private DynamicConfigFile _oldStoredData;
        private KeyValuePair<double, double> _balanceLimits;
        private MySqlConnection _mysqlConnection;
        private SQLiteConnection _sqlConnection;
        private StoredData _storedData;
        private bool _changed;
        private string _connectionStr;

        private class StoredData
        {
            public readonly Dictionary<string, double> Balances = new Dictionary<string, double>();
        }

        private void SaveData()
        {
            if (_changed)
            {
                Log("Saving balances for players...");
                Interface.Oxide.DataFileSystem.WriteObject(Name, _storedData); // TODO: Try/catch potential IOException
                _changed = false;
            }
        }

        private void OnServerSave() => SaveData();

        private void Unload() => SaveData();

        #endregion Stored Data

        #region Localization

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["CommandBalance"] = "balance",
                ["CommandDeposit"] = "deposit",
                ["CommandSetBalance"] = "SetBalance",
                ["CommandTransfer"] = "transfer",
                ["CommandWithdraw"] = "withdraw",
                ["CommandWipe"] = "ecowipe",
                ["DataSaved"] = "Economics data saved!",
                ["DataWiped"] = "Economics data wiped!",
                ["DepositedToAll"] = "Deposited {0:C} total ({1:C} each) to {2} player(s)",
                ["LogDeposit"] = "{0:C} deposited to {1}",
                ["LogSetBalance"] = "{0:C} set as balance for {1}",
                ["LogTransfer"] = "{0:C} transferred to {1} from {2}",
                ["LogWithdrawl"] = "{0:C} withdrawn from {1}",
                ["NegativeBalance"] = "Balance can not be negative!",
                ["NotAllowed"] = "You are not allowed to use the '{0}' command",
                ["NoPlayersFound"] = "No players found with name or ID '{0}'",
                ["PlayerBalance"] = "Balance for {0}: {1:C}",
                ["PlayerLacksMoney"] = "'{0}' does not have enough money!",
                ["PlayersFound"] = "Multiple players were found, please specify: {0}",
                ["ReceivedFrom"] = "You have received {0} from {1}",
                ["SetBalanceForAll"] = "Balance set to {0:C} for {1} player(s)",
                ["TransactionFailed"] = "Transaction failed! Make sure amount is above 0",
                ["TransferredTo"] = "{0} transferred to {1}",
                ["TransferredToAll"] = "Transferred {0:C} total ({1:C} each) to {2} player(s)",
                ["TransferToSelf"] = "You can not transfer money yourself!",
                ["UsageBalance"] = "{0} - check your balance",
                ["UsageBalanceOthers"] = "{0} <player name or id> - check balance of a player",
                ["UsageDeposit"] = "{0} <player name or id> <amount> - deposit amount to player",
                ["UsageSetBalance"] = "Usage: {0} <player name or id> <amount> - set balance for player",
                ["UsageTransfer"] = "Usage: {0} <player name or id> <amount> - transfer money to player",
                ["UsageWithdraw"] = "Usage: {0} <player name or id> <amount> - withdraw money from player",
                ["UsageWipe"] = "Usage: {0} - wipe all economics data",
                ["YouLackMoney"] = "You do not have enough money!",
                ["YouLostMoney"] = "You lost: {0:C}",
                ["YouReceivedMoney"] = "You received: {0:C}",
                ["YourBalance"] = "Your balance is: {0:C}",
                ["WithdrawnForAll"] = "Withdrew {0:C} total ({1:C} each) from {2} player(s)",
                ["ZeroAmount"] = "Amount cannot be zero"
            }, this);
        }

        #endregion Localization

        #region Initialization

        private const string PermissionBalance = "economics.balance";
        private const string PermissionDeposit = "economics.deposit";
        private const string PermissionDepositAll = "economics.depositall";
        private const string PermissionSetBalance = "economics.setbalance";
        private const string PermissionSetBalanceAll = "economics.setbalanceall";
        private const string PermissionTransfer = "economics.transfer";
        private const string PermissionTransferAll = "economics.transferall";
        private const string PermissionWithdraw = "economics.withdraw";
        private const string PermissionWithdrawAll = "economics.withdrawall";
        private const string PermissionWipe = "economics.wipe";

        private void Init()
        {
            // Register universal chat/console commands
            AddLocalizedCommand(nameof(CommandBalance));
            AddLocalizedCommand(nameof(CommandDeposit));
            AddLocalizedCommand(nameof(CommandSetBalance));
            AddLocalizedCommand(nameof(CommandTransfer));
            AddLocalizedCommand(nameof(CommandWithdraw));
            AddLocalizedCommand(nameof(CommandWipe));

            // Register permissions for commands
            permission.RegisterPermission(PermissionBalance, this);
            permission.RegisterPermission(PermissionDeposit, this);
            permission.RegisterPermission(PermissionDepositAll, this);
            permission.RegisterPermission(PermissionSetBalance, this);
            permission.RegisterPermission(PermissionSetBalanceAll, this);
            permission.RegisterPermission(PermissionTransfer, this);
            permission.RegisterPermission(PermissionTransferAll, this);
            permission.RegisterPermission(PermissionWithdraw, this);
            permission.RegisterPermission(PermissionWithdrawAll, this);
            permission.RegisterPermission(PermissionWipe, this);

            if (_config.UseSqLite && _config.UseMySql)
            {
                LogWarning("Both SQLite and MySQL databases are enabled; defaulting to SQLite for data storage");
            }

            if (_config.UseMySql || _config.UseSqLite)
            {
                Unsubscribe(nameof(OnServerSave));
                Unsubscribe(nameof(Unload));
            }

            bool emptyDatabase = true;
            if (_config.UseSqLite)
            {
                _connectionStr = $"Data Source={Interface.Oxide.DataDirectory}{Path.DirectorySeparatorChar}{Name}.db;";
                _sqlConnection = new SQLiteConnection(_connectionStr);
                _sqlConnection.Open();
                emptyDatabase = CheckDatabase();
            }
            else if (_config.UseMySql)
            {
                _connectionStr = $"server={_config.MySql.Server};uid={_config.MySql.Username};pwd={_config.MySql.Password};database={_config.MySql.Database};";
                _mysqlConnection = new MySqlConnection(_connectionStr);
                _mysqlConnection.Open();
                emptyDatabase = CheckDatabase();
            }

            if ((!_config.UseMySql && !_config.UseSqLite))
            {
                // Load existing data and migrate old data format
                _oldStoredData = Interface.Oxide.DataFileSystem.GetFile(Name);
                try
                {
                    Dictionary<ulong, double> temp = _oldStoredData.ReadObject<Dictionary<ulong, double>>();
                    try
                    {
                        _storedData = new StoredData();
                        foreach (KeyValuePair<ulong, double> old in temp)
                        {
                            if (!_storedData.Balances.ContainsKey(old.Key.ToString()))
                            {
                                _storedData.Balances.Add(old.Key.ToString(), old.Value);
                            }
                        }
                        _changed = true;
                    }
                    catch
                    {
                        // Ignore
                    }
                }
                catch
                {
                    _storedData = _oldStoredData.ReadObject<StoredData>();
                    _changed = true;
                }

                List<string> playerData = new List<string>(_storedData.Balances.Keys);

                // Check for and set any balances over maximum allowed
                if (_config.BalanceLimit > 0)
                {
                    foreach (string playerId in playerData)
                    {
                        if (_storedData.Balances[playerId] > _config.BalanceLimit)
                        {
                            _storedData.Balances[playerId] = _config.BalanceLimit;
                            _changed = true;
                        }
                    }
                }

                // Check for and remove any inactive player balance data
                if (_config.RemoveUnused)
                {
                    foreach (string playerId in playerData)
                    {
                        if (_storedData.Balances[playerId].Equals(_config.StartingBalance))
                        {
                            _storedData.Balances.Remove(playerId);
                            _changed = true;
                        }
                    }
                }
            }

            if (_config.UseSqLite)
            {
                if (emptyDatabase)
                {
                    // Migrate/import the data file, if present
                    foreach (KeyValuePair<string, double> playerData in _storedData.Balances)
                    {
                        using (SQLiteConnection connection = new SQLiteConnection(_connectionStr))
                        {
                            connection.Open();
                            string query = $"INSERT INTO balances VALUES ('{playerData.Key}', {playerData.Value})";
                            using (SQLiteCommand sqlCommand = new SQLiteCommand(query, connection))
                            {
                                sqlCommand.ExecuteNonQuery();
                            }
                        }
                    }
                }
                else if (_config.RemoveUnused)
                {
                    Log("Reading data from SQLite database...");

                    // Read the storedData from SQLite
                    _storedData = new StoredData();
                    using (SQLiteConnection connection = new SQLiteConnection(_connectionStr))
                    {
                        connection.Open();
                        using (SQLiteCommand sqlCommand = new SQLiteCommand("SELECT DISTINCT playerid, value FROM balances", connection))
                        using (SQLiteDataReader reader = sqlCommand.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                string playerId = reader.GetString(0);
                                double balance = reader.GetDouble(1);
                                _storedData.Balances.Add(playerId, balance);
                            }
                        }
                    }

                    // Check for and remove any inactive player balance data
                    foreach (string playerId in new List<string>(_storedData.Balances.Keys))
                    {
                        if (_storedData.Balances[playerId].Equals(_config.StartingBalance))
                        {
                            using (SQLiteConnection connection = new SQLiteConnection(_connectionStr))
                            {
                                connection.Open();
                                using (SQLiteCommand sqlCommand = new SQLiteCommand($"DELETE FROM balances WHERE playerid='{playerId}'", connection))
                                {
                                    sqlCommand.ExecuteNonQuery();
                                }
                            }
                        }
                    }
                    _storedData = new StoredData();
                }
            }
            else if (_config.UseMySql)
            {
                if (emptyDatabase)
                {
                    // Migrate/import the data file, if present
                    foreach (KeyValuePair<string, double> playerData in _storedData.Balances)
                    {
                        using (MySqlConnection connection = new MySqlConnection(_connectionStr))
                        {
                            connection.Open();
                            string query = $"INSERT INTO balances VALUES ('{playerData.Key}', {playerData.Value})";
                            using (MySqlCommand sqlCommand = new MySqlCommand(query, connection))
                            {
                                sqlCommand.ExecuteNonQuery();
                            }
                        }
                    }
                }
                else if (_config.RemoveUnused)
                {
                    Log("Reading data from MySQL database...");

                    _storedData = new StoredData();
                    using (MySqlConnection connection = new MySqlConnection(_connectionStr))
                    {
                        connection.Open();
                        using (MySqlCommand sqlCommand = new MySqlCommand("SELECT DISTINCT playerid, value FROM balances", connection))
                        using (MySqlDataReader reader = sqlCommand.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                string playerId = reader.GetString(0);
                                double balance = reader.GetDouble(1);
                                _storedData.Balances.Add(playerId, balance);
                            }
                        }
                    }

                    // Check for and remove any inactive player balance data
                    foreach (string playerId in new List<string>(_storedData.Balances.Keys))
                    {
                        if (_storedData.Balances[playerId].Equals(_config.StartingBalance))
                        {
                            using (MySqlConnection connection = new MySqlConnection(_connectionStr))
                            {
                                connection.Open();
                                using (MySqlCommand sqlCommand = new MySqlCommand($"DELETE FROM balances WHERE playerid='{playerId}'", connection))
                                {
                                    sqlCommand.ExecuteNonQuery();
                                }
                            }
                        }
                    }
                    _storedData = new StoredData();
                }
            }

            _balanceLimits = new KeyValuePair<double, double>(_config.NegativeBalanceLimit, _config.BalanceLimit);
        }

        private bool CheckDatabase()
        {
            // Return true if database is empty or non-existent
            bool found = false;

            if (_config.UseSqLite)
            {
                using (SQLiteConnection connection = new SQLiteConnection(_connectionStr))
                {
                    connection.Open();
                    using (SQLiteCommand command = new SQLiteCommand("SELECT name FROM sqlite_master WHERE type='table' AND name='balances'", connection))
                    using (SQLiteDataReader reader = command.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            found = true;
                        }
                    }
                }

                if (!found)
                {
                    SQLiteCommand command = new SQLiteCommand("DROP TABLE IF EXISTS balances", _sqlConnection);
                    command.ExecuteNonQuery();
                    command = new SQLiteCommand("CREATE TABLE balances (playerid varchar(32) PRIMARY KEY, value DOUBLE DEFAULT 0)", _sqlConnection);
                    command.ExecuteNonQuery();
                    return true;
                }
            }
            else if (_config.UseMySql)
            {
                using (MySqlConnection connection = new MySqlConnection(_connectionStr))
                {
                    connection.Open();
                    using (MySqlCommand command = new MySqlCommand($"SELECT * FROM information_schema.tables WHERE table_schema='{_config.MySql.Database}' AND table_name='balances' LIMIT 1;", connection))
                    using (MySqlDataReader reader = command.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            found = true;
                        }
                    }
                }

                if (!found)
                {
                    MySqlCommand command = new MySqlCommand("DROP TABLE IF EXISTS balances", _mysqlConnection);
                    command.ExecuteNonQuery();
                    command = new MySqlCommand("CREATE TABLE balances (playerid varchar(32) PRIMARY KEY, value DOUBLE DEFAULT 0)", _mysqlConnection);
                    command.ExecuteNonQuery();
                    return true;
                }
            }

            return !found;
        }

        private void OnNewSave()
        {
            if (_config.WipeOnNewSave)
            {
                if (_config.UseSqLite)
                {
                    SQLiteCommand command = new SQLiteCommand("DROP TABLE IF EXISTS balances", _sqlConnection);
                    command.ExecuteNonQuery();
                    command = new SQLiteCommand("CREATE TABLE balances (playerid varchar(32) PRIMARY KEY, value DOUBLE DEFAULT 0)", _sqlConnection);
                    command.ExecuteNonQuery();
                }
                else if (_config.UseMySql)
                {
                    SQLiteCommand command = new SQLiteCommand("DROP TABLE IF EXISTS balances", _sqlConnection);
                    command.ExecuteNonQuery();
                    command = new SQLiteCommand("CREATE TABLE balances (playerid varchar(32) PRIMARY KEY, value DOUBLE DEFAULT 0)", _sqlConnection);
                    command.ExecuteNonQuery();
                }
                else
                {
                    _storedData.Balances.Clear();
                    _changed = true;
                }

                Interface.Call("OnEconomicsDataWiped");
            }
        }

        #endregion Initialization

        #region API Methods

        private double Balance(string playerId)
        {
            if (string.IsNullOrEmpty(playerId))
            {
                LogWarning("Balance method called without a valid player ID");
                return 0.0;
            }

            double balance = _config.StartingBalance;

            if (_config.UseSqLite)
            {
                using (SQLiteConnection connection = new SQLiteConnection(_connectionStr))
                {
                    connection.Open();
                    using (SQLiteCommand sqlCommand = new SQLiteCommand($"SELECT value FROM balances WHERE playerid='{playerId}'", connection))
                    using (SQLiteDataReader reader = sqlCommand.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            balance = reader.GetDouble(0);
                        }
                    }
                }
            }
            else if (_config.UseMySql)
            {
                using (MySqlConnection connection = new MySqlConnection(_connectionStr))
                {
                    connection.Open();
                    using (MySqlCommand sqlCommand = new MySqlCommand($"SELECT value FROM balances WHERE playerid='{playerId}'", connection))
                    using (MySqlDataReader reader = sqlCommand.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            balance = reader.GetDouble(0);
                        }
                    }
                }
            }
            else
            {
                _storedData.Balances.TryGetValue(playerId, out balance);
            }

            // TODO: Handle potential database read failure; check if balance changed or not
            return balance;
        }

        private double Balance(ulong playerId) => Balance(playerId.ToString());

        private KeyValuePair<double, double> BalanceLimits() => _balanceLimits;

        private bool Deposit(string playerId, double amount)
        {
            if (string.IsNullOrEmpty(playerId))
            {
                LogWarning("Deposit method called without a valid player ID");
                return false;
            }

            if (amount > 0 && SetBalance(playerId, amount + Balance(playerId)))
            {
                Interface.Call("OnEconomicsDeposit", playerId, amount);

                if (_config.LogTransactions)
                {
                    LogToFile("transactions", $"[{DateTime.Now}] {GetLang("LogDeposit", null, amount, playerId)}", this);
                }

                return true;
            }

            return false;
        }

        private bool Deposit(ulong playerId, double amount) => Deposit(playerId.ToString(), amount);

        private bool SetBalance(string playerId, double amount)
        {
            if (string.IsNullOrEmpty(playerId))
            {
                LogWarning("SetBalance method called without a valid player ID");
                return false;
            }

            amount = Math.Round(amount, 2);

            if (amount < 0 && _config.NegativeBalanceLimit == 0)
            {
                return false;
            }

            if (_config.BalanceLimit > 0 && amount > _config.BalanceLimit)
            {
                amount = _config.BalanceLimit;
            }
            else if (_config.NegativeBalanceLimit < 0 && amount < _config.NegativeBalanceLimit)
            {
                amount = _config.NegativeBalanceLimit;
            }

            double oldAmount = Balance(playerId);

            if (oldAmount > _config.StartingBalance)
            {
                if (_config.UseSqLite)
                {
                    using (SQLiteConnection connection = new SQLiteConnection(_connectionStr))
                    {
                        connection.Open();
                        //string query = $"UPDATE balances SET value={amount} WHERE playerid='{playerId}'";
                        //string query = $"INSERT INTO balances VALUES ('{playerId}', {amount})";
                        //string query = $"INSERT INTO balances (playerid, value) VALUES ('{playerId}', {amount})"
                        //    + "ON CONFLICT (playerid) DO UPDATE SET value=excluded.value";
                        string query = $"REPLACE INTO balances (playerid, value) VALUES('{playerId}', {amount})";
                        using (SQLiteCommand sqlCommand = new SQLiteCommand(query, connection))
                        {
                            sqlCommand.ExecuteNonQuery();
                        }
                    }
                }
                else if (_config.UseMySql)
                {
                    using (MySqlConnection connection = new MySqlConnection(_connectionStr))
                    {
                        connection.Open();
                        //string query = $"UPDATE balances SET value={amount} WHERE playerid='{playerId}'";
                        //string query = $"INSERT INTO balances VALUES ('{playerId}', {amount})";
                        //string query = $"INSERT INFO balances VALUES ('{playerId}', {amount})"
                        //    + $"ON DUPLICATE KEY UPDATE playerid='{playerId}', value={amount}";
                        string query = $"REPLACE INTO balances (playerid, value) VALUES('{playerId}', {amount})";
                        using (MySqlCommand sqlCommand = new MySqlCommand(query, connection))
                        {
                            sqlCommand.ExecuteNonQuery();
                        }
                    }
                }
                else
                {
                    if (_storedData.Balances.ContainsKey(playerId))
                    {
                        _storedData.Balances[playerId] = amount;
                    }
                    else
                    {
                        _storedData.Balances.Add(playerId, amount);
                    }
                    _changed = true;
                }

                Interface.Call("OnEconomicsBalanceUpdated", playerId, amount, oldAmount);
                Interface.CallDeprecatedHook("OnBalanceChanged", "OnEconomicsBalanceUpdated", new DateTime(2023, 1, 1), playerId, amount);

                if (_config.LogTransactions)
                {
                    LogToFile("transactions", $"[{DateTime.Now}] {GetLang("LogSetBalance", null, amount, playerId)}", this);
                }
            }

            return true;
        }

        private bool SetBalance(ulong playerId, double amount) => SetBalance(playerId.ToString(), amount);

        private bool Transfer(string playerId, string targetId, double amount)
        {
            if (string.IsNullOrEmpty(playerId))
            {
                LogWarning("Transfer method called without a valid player ID");
                return false;
            }

            if (Withdraw(playerId, amount) && Deposit(targetId, amount))
            {
                Interface.Call("OnEconomicsTransfer", playerId, targetId, amount);

                if (_config.LogTransactions)
                {
                    LogToFile("transactions", $"[{DateTime.Now}] {GetLang("LogTransfer", null, amount, targetId, playerId)}", this);
                }

                return true;
            }

            return false;
        }

        private bool Transfer(ulong playerId, ulong targetId, double amount)
        {
            return Transfer(playerId.ToString(), targetId.ToString(), amount);
        }

        private bool Withdraw(string playerId, double amount)
        {
            if (string.IsNullOrEmpty(playerId))
            {
                LogWarning("Withdraw method called without a valid player ID");
                return false;
            }

            double balance = Balance(playerId);
            if (amount >= 0 || balance <= _config.NegativeBalanceLimit && _config.NegativeBalanceLimit < 0)
            {
                if (balance >= amount && SetBalance(playerId, balance - amount) || _config.NegativeBalanceLimit <= balance - amount && SetBalance(playerId, balance - amount))
                {
                    Interface.Call("OnEconomicsWithdrawl", playerId, amount);

                    if (_config.LogTransactions)
                    {
                        LogToFile("transactions", $"[{DateTime.Now}] {GetLang("LogWithdrawl", null, amount, playerId)}", this);
                    }

                    return true;
                }
            }

            return false;
        }

        private bool Withdraw(ulong playerId, double amount) => Withdraw(playerId.ToString(), amount);

        #endregion API Methods

        #region Commands

        #region Balance Command

        private void CommandBalance(IPlayer player, string command, string[] args)
        {
            if (args != null && args.Length > 0)
            {
                if (!player.HasPermission(PermissionBalance))
                {
                    Message(player, "NotAllowed", command);
                    return;
                }

                IPlayer target = FindPlayer(args[0], player);
                if (target == null)
                {
                    Message(player, "UsageBalance", command);
                    return;
                }

                Message(player, "PlayerBalance", target.Name, Balance(target.Id));
                return;
            }

            if (player.IsServer)
            {
                Message(player, "UsageBalanceOthers", command);
            }
            else
            {
                Message(player, "YourBalance", Balance(player.Id));
            }
        }

        #endregion Balance Command

        #region Deposit Command

        private void CommandDeposit(IPlayer player, string command, string[] args)
        {
            if (!player.HasPermission(PermissionDeposit))
            {
                Message(player, "NotAllowed", command);
                return;
            }

            if (args == null || args.Length <= 1)
            {
                Message(player, "UsageDeposit", command);
                return;
            }

            double amount;
            double.TryParse(args[1], out amount);

            if (amount <= 0)
            {
                Message(player, "ZeroAmount");
                return;
            }

            if (args[0] == "*")
            {
                if (!player.HasPermission(PermissionDepositAll))
                {
                    Message(player, "NotAllowed", command);
                    return;
                }

                int receivers = 0;

                if (_config.UseSqLite)
                {
                    using (SQLiteConnection connection = new SQLiteConnection(_connectionStr))
                    {
                        connection.Open();
                        // TODO: INSERT OR UPDATE
                        string query = $"UPDATE balances SET value=value+{amount}";
                        using (SQLiteCommand sqlCommand = new SQLiteCommand(query, connection))
                        {
                            sqlCommand.ExecuteNonQuery();
                        }

                        using (SQLiteCommand sqlCommand = new SQLiteCommand("SELECT COUNT(*) FROM balances", connection))
                        using (SQLiteDataReader reader = sqlCommand.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                receivers = reader.GetInt32(0);
                            }
                        }
                    }
                }
                else if (_config.UseMySql)
                {
                    using (MySqlConnection connection = new MySqlConnection(_connectionStr))
                    {
                        connection.Open();
                        // TODO: INSERT OR UPDATE
                        string query = $"UPDATE balances SET value=value+{amount}";
                        using (MySqlCommand sqlCommand = new MySqlCommand(query, connection))
                        {
                            sqlCommand.ExecuteNonQuery();
                        }

                        using (MySqlCommand sqlCommand = new MySqlCommand("SELECT COUNT(*) FROM balances", connection))
                        using (MySqlDataReader reader = sqlCommand.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                receivers = reader.GetInt32(0);
                            }
                        }
                    }
                }
                else
                {
                    foreach (string targetId in _storedData.Balances.Keys.ToList())
                    {
                        if (Deposit(targetId, amount))
                        {
                            receivers++;
                        }
                    }
                }

                Message(player, "DepositedToAll", amount * receivers, amount, receivers);
            }
            else
            {
                IPlayer target = FindPlayer(args[0], player);
                if (target == null)
                {
                    return;
                }

                if (Deposit(target.Id, amount))
                {
                    Message(player, "PlayerBalance", target.Name, Balance(target.Id));
                }
                else
                {
                    Message(player, "TransactionFailed", target.Name);
                }
            }
        }

        #endregion Deposit Command

        #region Set Balance Command

        private void CommandSetBalance(IPlayer player, string command, string[] args)
        {
            if (!player.HasPermission(PermissionSetBalance))
            {
                Message(player, "NotAllowed", command);
                return;
            }

            if (args == null || args.Length <= 1)
            {
                Message(player, "UsageSetBalance", command);
                return;
            }

            double amount;
            double.TryParse(args[1], out amount);

            if (amount < 0)
            {
                Message(player, "NegativeBalance");
                return;
            }

            if (args[0] == "*")
            {
                if (!player.HasPermission(PermissionSetBalanceAll))
                {
                    Message(player, "NotAllowed", command);
                    return;
                }

                int receivers = 0;

                if (_config.UseSqLite)
                {
                    using (SQLiteConnection connection = new SQLiteConnection(_connectionStr))
                    {
                        connection.Open();
                        // TODO: INSERT OR UPDATE
                        string query = $"UPDATE balances SET value={amount}";
                        using (SQLiteCommand sqlCommand = new SQLiteCommand(query, connection))
                        {
                            sqlCommand.ExecuteNonQuery();
                        }

                        using (SQLiteCommand sqlCommand = new SQLiteCommand("SELECT COUNT(*) FROM balances", connection))
                        using (SQLiteDataReader reader = sqlCommand.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                receivers = reader.GetInt32(0);
                            }
                        }
                    }
                }
                else if (_config.UseMySql)
                {
                    using (MySqlConnection connection = new MySqlConnection(_connectionStr))
                    {
                        connection.Open();
                        // TODO: INSERT OR UPDATE
                        string query = $"UPDATE balances SET value={amount}";
                        using (MySqlCommand sqlCommand = new MySqlCommand(query, connection))
                        {
                            sqlCommand.ExecuteNonQuery();
                        }

                        using (MySqlCommand sqlCommand = new MySqlCommand("SELECT COUNT(*) FROM balances", connection))
                        using (MySqlDataReader reader = sqlCommand.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                receivers = reader.GetInt32(0);
                            }
                        }
                    }
                }
                else
                {
                    foreach (string targetId in _storedData.Balances.Keys.ToList())
                    {
                        if (SetBalance(targetId, amount))
                        {
                            receivers++;
                        }
                    }
                }

                Message(player, "SetBalanceForAll", amount, receivers);
            }
            else
            {
                IPlayer target = FindPlayer(args[0], player);
                if (target == null)
                {
                    return;
                }

                if (SetBalance(target.Id, amount))
                {
                    Message(player, "PlayerBalance", target.Name, Balance(target.Id));
                }
                else
                {
                    Message(player, "TransactionFailed", target.Name);
                }
            }
        }

        #endregion Set Balance Command

        #region Transfer Command

        private void CommandTransfer(IPlayer player, string command, string[] args)
        {
            if (!player.HasPermission(PermissionTransfer))
            {
                Message(player, "NotAllowed", command);
                return;
            }

            if (args == null || args.Length <= 1)
            {
                Message(player, "UsageTransfer", command);
                return;
            }

            double amount;
            double.TryParse(args[1], out amount);

            if (amount <= 0)
            {
                Message(player, "ZeroAmount");
                return;
            }

            if (args[0] == "*")
            {
                if (!player.HasPermission(PermissionTransferAll))
                {
                    Message(player, "NotAllowed", command);
                    return;
                }

                if (!Withdraw(player.Id, amount))
                {
                    Message(player, "YouLackMoney");
                    return;
                }

                int receivers = players.Connected.Count();
                double splitAmount = amount /= receivers;

                foreach (IPlayer target in players.Connected)
                {
                    if (Deposit(target.Id, splitAmount))
                    {
                        if (target.IsConnected)
                        {
                            Message(target, "ReceivedFrom", splitAmount, player.Name);
                        }
                    }
                }
                Message(player, "TransferedToAll", amount, splitAmount, receivers);
            }
            else
            {
                IPlayer target = FindPlayer(args[0], player);
                if (target == null)
                {
                    return;
                }

                if (target.Equals(player))
                {
                    Message(player, "TransferToSelf");
                    return;
                }

                if (!Withdraw(player.Id, amount))
                {
                    Message(player, "YouLackMoney");
                    return;
                }

                if (Deposit(target.Id, amount))
                {
                    Message(player, "TransferredTo", amount, target.Name);
                    Message(target, "ReceivedFrom", amount, player.Name);
                }
                else
                {
                    Message(player, "TransactionFailed", target.Name);
                }
            }
        }

        #endregion Transfer Command

        #region Withdraw Command

        private void CommandWithdraw(IPlayer player, string command, string[] args)
        {
            if (!player.HasPermission(PermissionWithdraw))
            {
                Message(player, "NotAllowed", command);
                return;
            }

            if (args == null || args.Length <= 1)
            {
                Message(player, "UsageWithdraw", command);
                return;
            }

            double amount;
            double.TryParse(args[1], out amount);

            if (amount <= 0)
            {
                Message(player, "ZeroAmount");
                return;
            }

            if (args[0] == "*")
            {
                if (!player.HasPermission(PermissionWithdrawAll))
                {
                    Message(player, "NotAllowed", command);
                    return;
                }

                int receivers = 0;

                if (_config.UseSqLite)
                {
                    using (SQLiteConnection connection = new SQLiteConnection(_connectionStr))
                    {
                        connection.Open();
                        // TODO: INSERT OR UPDATE
                        string query = $"UPDATE balances SET value=value-{amount}";
                        using (SQLiteCommand sqlCommand = new SQLiteCommand(query, connection))
                        {
                            sqlCommand.ExecuteNonQuery();
                        }

                        using (SQLiteCommand sqlCommand = new SQLiteCommand("SELECT COUNT(*) FROM balances", connection))
                        using (SQLiteDataReader reader = sqlCommand.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                receivers = reader.GetInt32(0);
                            }
                        }
                    }
                }
                else if (_config.UseMySql)
                {
                    using (MySqlConnection connection = new MySqlConnection(_connectionStr))
                    {
                        connection.Open();
                        // TODO: INSERT OR UPDATE
                        string query = $"UPDATE balances SET value=value-{amount}";
                        using (MySqlCommand sqlCommand = new MySqlCommand(query, connection))
                        {
                            sqlCommand.ExecuteNonQuery();
                        }

                        using (MySqlCommand sqlCommand = new MySqlCommand("SELECT COUNT(*) FROM balances", connection))
                        using (MySqlDataReader reader = sqlCommand.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                receivers = reader.GetInt32(0);
                            }
                        }
                    }
                }
                else
                {
                    foreach (string targetId in _storedData.Balances.Keys.ToList())
                    {
                        if (Withdraw(targetId, amount))
                        {
                            receivers++;
                        }
                    }
                }

                Message(player, "WithdrawnForAll", amount * receivers, amount, receivers);
            }
            else
            {
                IPlayer target = FindPlayer(args[0], player);
                if (target == null)
                {
                    return;
                }

                if (Withdraw(target.Id, amount))
                {
                    Message(player, "PlayerBalance", target.Name, Balance(target.Id));
                }
                else
                {
                    Message(player, "YouLackMoney", target.Name);
                }
            }
        }

        #endregion Withdraw Command

        #region Wipe Command

        private void CommandWipe(IPlayer player, string command, string[] args)
        {
            if (!player.HasPermission(PermissionWipe))
            {
                Message(player, "NotAllowed", command);
                return;
            }

            if (_config.UseSqLite)
            {
                using (SQLiteConnection connection = new SQLiteConnection(_connectionStr))
                {
                    connection.Open();
                    using (SQLiteCommand sqlCommand = new SQLiteCommand("DELETE FROM balances", connection))
                    {
                        sqlCommand.ExecuteNonQuery();
                    }
                }
            }
            else if (_config.UseMySql)
            {
                using (MySqlConnection connection = new MySqlConnection(_connectionStr))
                {
                    connection.Open();
                    using (MySqlCommand sqlCommand = new MySqlCommand("DELETE FROM balances", connection))
                    {
                        sqlCommand.ExecuteNonQuery();
                    }
                }
            }
            else
            {
                _storedData = new StoredData();
                _changed = true;
                SaveData();
            }

            Message(player, "DataWiped");
            Interface.Call("OnEconomicsDataWiped", player);
        }

        #endregion Wipe Command

        #endregion Commands

        #region Helpers

        private void AddLocalizedCommand(string command)
        {
            foreach (string language in lang.GetLanguages(this))
            {
                foreach (KeyValuePair<string, string> message in lang.GetMessages(language, this))
                {
                    if (message.Key.Equals(command) && !string.IsNullOrEmpty(message.Value))
                    {
                        AddCovalenceCommand(message.Value, command);
                    }
                }
            }
        }

        private IPlayer FindPlayer(string playerNameOrId, IPlayer player)
        {
            IPlayer[] foundPlayers = players.FindPlayers(playerNameOrId).ToArray();
            if (foundPlayers.Length > 1)
            {
                Message(player, "PlayersFound", string.Join(", ", foundPlayers.Select(p => p.Name).Take(10).ToArray()).Truncate(60));
                return null;
            }

            IPlayer target = foundPlayers.Length == 1 ? foundPlayers[0] : null;
            if (target == null)
            {
                Message(player, "NoPlayersFound", playerNameOrId);
                return null;
            }

            return target;
        }

        private string GetLang(string langKey, string playerId = null, params object[] args)
        {
            return string.Format(lang.GetMessage(langKey, this, playerId), args);
        }

        private void Message(IPlayer player, string textOrLang, params object[] args)
        {
            if (player.IsConnected)
            {
                string message = GetLang(textOrLang, player.Id, args);
                player.Reply(message != textOrLang ? message : textOrLang);
            }
        }

        #endregion Helpers
    }
}

#region Extension Methods

namespace Oxide.Plugins.EconomicsExtensionMethods
{
    public static class ExtensionMethods
    {
        public static T Clamp<T>(this T val, T min, T max) where T : IComparable<T>
        {
            if (val.CompareTo(min) < 0)
            {
                return min;
            }

            if (val.CompareTo(max) > 0)
            {
                return max;
            }

            return val;
        }
    }
}

#endregion Extension Methods
