/***
	 * OnDisable
	 */
	@Override
	public void onDisable() {
		
		// kicks all players when the plugin is disabled (server is shutdown)
		for (Player player : Bukkit.getOnlinePlayers()){
			player.kickPlayer(ChatColor.RED + _("shutdownKickMessage", player.getName()));
		}
		// if we don't have per-player language loaded from DB, do not try to load it now :-)
		avoidDB = true;
		
		// execute everything that should be executed on disable
		if (onDisableFunctions.size() > 0) {
			Class<?>[] proto = new Class[] {this.getClass()};
			Object[] params = new Object[] {this};
			
			for (String s : onDisableFunctions) {
				try {
					String[] ss = s.split("#####");
					Class<?> c = Class.forName(ss[0]);
					Method method = c.getDeclaredMethod(ss[1], proto);
					method.invoke(null, params);
				} catch (Throwable e) {
					LogHelper.logSevere("[CommandsEX] " + _("errorFunctionOnDisableExecute", "") + s);
					LogHelper.logDebug("Message: " + e.getMessage() + ", cause: " + e.getCause());
				}
			}
		}
		
		// close all database connections
		LogHelper.logInfo("[" + this.getDescription().getName() + "] " + _("disableMsg", ""));
	}
	public void stopTimer(){
		stopTime = System.currentTimeMillis();
		finalTime = stopTime - startTime;
		if (getConf().getBoolean("startupTimer")){
			LogHelper.logInfo("[CommandsEx] " + _("startupTime", "") + finalTime + "ms");
		}
	}
	/***
	 * When a player leaves the server, their join timestamp and playtime is removed to free up memory.
	 * Playtime will get stored into database if the time since his joining is more than 45 seconds
	 * to prevent database overloading when player tries to spam logins/logouts.
	 * @param e
	 */
	@EventHandler(priority = EventPriority.NORMAL)
	public void pQuit(PlayerQuitEvent e) {
		String pName = e.getPlayer().getName();
		Integer stamp = Utils.getUnixTimestamp(0L);

		// schedule player's IP removal
		CommandsEX.plugin.getServer().getScheduler().scheduleSyncDelayedTask(CommandsEX.plugin, new DelayedIpRemoval(pName), (20 * getConf().getInt("maxIPholdTime")));
		
		// save player's playtime
		if (sqlEnabled && joinTimes.containsKey(pName)) {
			Integer played = (stamp - joinTimes.get(pName));
			if (played >= minTimeToSavePlayTime) {
				// player was online for more than minTimeToSavePlayTime seconds, count this visit
				if (playTimes.containsKey(pName) && (playTimes.get(pName) > -1)) {
					// update playtime directly if we have previous time loaded
					playTimes.put(pName, (playTimes.get(pName) + played));
				} else {
					// get total playtime from database, since we don't have it loaded yet
					try {
						// first, reset the time, so we don't add to -1 later
						Integer pTime = 0;
						ResultSet res = SQLManager.query_res("SELECT seconds_played FROM " + SQLManager.prefix + "playtime WHERE player_name = ?", pName);
						while (res.next()) {
							pTime = res.getInt("seconds_played");
						}
						res.close();
						playTimes.put(pName, (pTime + played));
					} catch (Throwable ex) {
						// something went wrong...
						LogHelper.logSevere("[CommandsEX] " + _("dbTotalPlayTimeGetError", ""));
						LogHelper.logDebug("Message: " + ex.getMessage() + ", cause: " + ex.getCause());
					}
				}
				// update DB with new value
				played = playTimes.get(pName);
				SQLManager.query("INSERT "+ (SQLManager.sqlType.equals("mysql") ? "" : "OR REPLACE ") +"INTO " + SQLManager.prefix + "playtime VALUES (?, ?)"+ (SQLManager.sqlType.equals("mysql") ? " ON DUPLICATE KEY UPDATE seconds_played = VALUES(seconds_played)" : ""), pName, played);
			}
			joinTimes.remove(pName);
			playTimes.remove(pName);
		}
	}
  /**
   * * Extinguish - Allows a player (or console) to extinguish themself or another player.
   *
   * @author iKeirNez
   * @param sender
   * @param args
   * @return
   */
  public static Boolean run(CommandSender sender, String alias, String[] args) {

    if (sender instanceof Player) {
      Player player = (Player) sender;
      if (Utils.checkCommandSpam(player, "cex_extinguish")) {
        return true;
      }
    }

    if (args.length == 0) {
      if (sender instanceof Player) {
        Player player = (Player) sender;
        player.setFireTicks(0);
        LogHelper.showInfo("extExtinguished", player, ChatColor.GREEN);
      } else {
        LogHelper.showInfo("playerNameMissing", sender, ChatColor.RED);
      }
    } else if (args.length == 1) {
      Player toExt = Bukkit.getPlayer(args[0]);
      if (toExt != null) {
        // Prevents the player from recieving 2 messages if they do /ext <their-player-name>
        if (toExt != sender) {
          if ((!(sender instanceof Player))
              || ((Player) sender).hasPermission("cex.extinguish.others")) {
            toExt.setFireTicks(0);
            LogHelper.showInfo(
                "extExtinguishedBySomeoneElse#####[ " + sender.getName(), toExt, ChatColor.GREEN);
            LogHelper.showInfo(
                "extExtinguishedSomeoneElse#####[ " + toExt.getName(), sender, ChatColor.GREEN);
          } else {
            LogHelper.showInfo("extOtherNoPerm", sender, ChatColor.RED);
          }
        } else {
          toExt.setFireTicks(0);
          LogHelper.showInfo("extExtinguished", sender, ChatColor.GREEN);
        }
      } else {
        LogHelper.showInfo("invalidPlayer", sender, ChatColor.RED);
      }
    } else {
      LogHelper.showInfo("incorrectUsage", sender, ChatColor.RED);
    }

    return true;
  }
	/***
	 * The main function which replaces chat with matched replacements.
	 * @param e
	 * @return
	 */
	@EventHandler(priority = EventPriority.LOWEST)
	public void replaceChat(PlayerChatEvent e) {
		if (e.isCancelled()) return;
		
		try {
			ScriptEnvironment env = new ScriptEnvironment(); {
				env.setCommandSender(e.getPlayer());
				env.setServer(e.getPlayer().getServer());
			}
			ArrayList<ReplacementPair> preparedEffects = new ArrayList<ReplacementPair>(); //holds all effects until all replacements done

			for (ReplacementPair rp : pairs) {
				StringBuffer sb = new StringBuffer();
				Matcher m = rp.getRegex().matcher(e.getMessage());
				if (!m.find()) continue;
				env.setMatcher(m);

				if (rp.playerWillVanish()) { //the player will vanish as a result of this, special handling
					int cutlen = CommandsEX.getConf().getInt("replacements.cutoff.length", 1);
					String cuttext = CommandsEX.getConf().getString("replacements.cutoff.indicator", "--*");
	
					String rep = m.group().substring(0, cutlen).concat(cuttext);
					m.appendReplacement(sb, rep);
					e.setMessage(sb.toString());
					//e.setCancelled(true);
					//e.getPlayer().chat(sb.toString()); //chat first
	
					rp.executeEffects(env); //then execute the replacement
					return;
				}

				//loop through with find/replace
				do { //use do while, due to the find() invocation above
					// test if it is all upper, and replace with all upper (if we have this set up in the regex itself - in config file)
					if (rp.getSameOutputCase() && allUpper && m.group().toUpperCase().equals(m.group())) {
						m.appendReplacement(sb, rp.executeString(env).toUpperCase());
					} else {
						m.appendReplacement(sb, rp.executeString(env));
					}
				} while (m.find());
				m.appendTail(sb);

				if (!preparedEffects.contains(rp)) {
					preparedEffects.add(rp);
				}
				e.setMessage(sb.toString());
			}
			
			//after all replacements are in: execute the effects
			if (!preparedEffects.isEmpty()) {
				//e.setCancelled(true);
				//e.getPlayer().chat(sb.toString()); //chat first
	
				env.setMatcher(null);
				for (ReplacementPair rp : preparedEffects){
					rp.executeEffects(env);
				}
			}
		} catch (Exception ex){
			LogHelper.logSevere("[CommandsEX] " + _("cmdOrChatreplacementFailed", ""));
			LogHelper.logDebug("Message: " + ex.getMessage() + ", cause: " + ex.getCause());
		}
	}
	/***
	 * OnEnable
	 */
	@Override
	public void onEnable() {
		startTimer();
		// save default config if not saved yet
		getConfig().options().copyDefaults(true);
		saveConfig();
		
		// check for Vault plugin presence
		try {
			new Vault();
			vaultPresent = true;
		} catch (Throwable e) {}
		
		// set up commands listener
		cListener = new Commands(this);

		// initialize translations
		Language.init(this);

		// get description file and display initial startup OK info
		pdfFile = this.getDescription();
		LogHelper.logInfo("[" + pdfFile.getName() + "] " + _("startupMessage", "") + " " + Language.defaultLocale);
		LogHelper.logInfo("[" + pdfFile.getName() + "] " + _("version", "") + " " + pdfFile.getVersion() + " " + _("enableMsg", ""));

		// initialize database, if we have it included in our build
		Class<?>[] proto = new Class[] {this.getClass()};
		Object[] params = new Object[] {this};
		if (getConf().getBoolean("enableDatabase")) {
			try {
				Class<?> c = Class.forName("com.github.zathrus_writer.commandsex.SQLManager");
				Method method = c.getDeclaredMethod("init", proto);
				method.invoke(null, params);
			} catch (ClassNotFoundException e) {
				// this is OK, since we won't neccessarily have this class in each build
			} catch (Throwable e) {
				LogHelper.logSevere(_("dbError", ""));
				LogHelper.logDebug("Message: " + e.getMessage() + ", cause: " + e.getCause());
			}
		}
		
		// enable existing classes that are listening to events - determine names from permissions
		// ... also call init() function for each helper class that requires initialization (has Init prefix in permissions)
		List<Permission> perms = CommandsEX.pdfFile.getPermissions();
		for(int i = 0; i <= perms.size() - 1; i++) {
			// call initialization function for each of the event handling functions
			String pName = perms.get(i).getName();
			if (pName.startsWith("Listener")) {
				String[] s = pName.split("\\.");
				if (s.length == 0) continue;
				try {
					Class.forName("com.github.zathrus_writer.commandsex.handlers.Handler_" + s[1]).newInstance();
				} catch (ClassNotFoundException e) {
					// this is OK, since we won't neccessarily have this class in each build
				} catch (Throwable e) {
					LogHelper.logSevere(_("loadTimeError", ""));
					LogHelper.logDebug("Message: " + e.getMessage() + ", cause: " + e.getCause());
				}
			} else if (pName.startsWith("Init")) {
				String[] s = pName.split("\\.");
				if (s.length == 0) continue;
				try {
					Class<?> c = Class.forName("com.github.zathrus_writer.commandsex.helpers." + s[1]);
					Method method = c.getDeclaredMethod("init", proto);
					method.invoke(null, params);
				} catch (ClassNotFoundException e) {
					// this is OK, since we won't neccessarily have this class in each build
				} catch (Throwable e) {
					LogHelper.logSevere(_("loadTimeError", ""));
					LogHelper.logDebug("Message: " + e.getMessage() + ", cause: " + e.getCause());
				}
			}
		}
		
		// setup a recurring task that will periodically save players' play times into DB
		if (sqlEnabled) {
			getServer().getScheduler().scheduleAsyncRepeatingTask(this, new Runnable() {
				@Override
			    public void run() {
			        // flush play times only for players that have their playtime loaded initially
					Integer stamp = Utils.getUnixTimestamp(0L);
					Iterator<Entry<String, Integer>> it = CommandsEX.playTimes.entrySet().iterator();
					List<Object> insertParts = new ArrayList<Object>();
					List<Object> insertValues = new ArrayList<Object>();
					while (it.hasNext()) {
						Map.Entry<String, Integer> pairs = (Map.Entry<String, Integer>)it.next();
						
						// only update data for players that don't have -1 set as their playTime
						if (pairs.getValue() <= -1) continue;
						
						String pName = pairs.getKey();
						// update play time and join time
						Integer played = (pairs.getValue() + (stamp - CommandsEX.joinTimes.get(pName)));
						CommandsEX.playTimes.put(pName, played);
						CommandsEX.joinTimes.put(pName, Utils.getUnixTimestamp(0L));
						
						// prepare DB query parts
						insertParts.add("SELECT ? AS 'player_name', ? AS 'seconds_played'");
						insertValues.add(pName);
						insertValues.add(played);
						//it.remove(); // avoids a ConcurrentModificationException - not needed in our case and will clear out HashMap!
					}
					
					if (insertParts.size() > 0) {
						// update the database
						SQLManager.query("INSERT "+ (SQLManager.sqlType.equals("mysql") ? "" : "OR REPLACE ") +"INTO " + SQLManager.prefix + "playtime "+ Utils.implode(insertParts, " UNION ") + (SQLManager.sqlType.equals("mysql") ? " ON DUPLICATE KEY UPDATE seconds_played = VALUES(seconds_played)" : ""), insertValues);
					}
			    }
			}, (20 * playTimesFlushTime), (20 * playTimesFlushTime));
			
			// tell Bukkit we have some event handling to do in this class :-)
			this.getServer().getPluginManager().registerEvents(this, this);
		}
		
		try {
		    Metrics metrics = new Metrics(plugin);
		    metrics.start();
		} catch (IOException e) {

		}
		stopTimer();
	}
	/***
	 * When a player joins the server, their join timestamp is saved, helping us to determine
	 * how long he's been online. Also, a new delayed task will be created that will load up player's
	 * full playtime from database, should the player stay on the server for more than minTimeFromLogout seconds.
	 * 
	 * Additionally, when a player joins the server, their IP is stored internally to allow for IP-banning when
	 * the player leaves as soon as they burst-grief.
	 * @param e
	 */
	@EventHandler(priority = EventPriority.NORMAL)
	public void pJoin(PlayerJoinEvent e) {
		String pName = e.getPlayer().getName();
		playerIPs.put(pName.toLowerCase(), e.getPlayer().getAddress().getAddress().getHostAddress());

		// check if player is not jailed
		try {
			if (Jails.jailedPlayers.containsKey(pName)) {
				LogHelper.showInfo("jailsStillJailed", e.getPlayer());
			}
		} catch (Throwable ex) {}
		
		if (sqlEnabled) {
			// add -1 to playtimes, so our Runnable function will know to look the player up after the delay has passed
			playTimes.put(pName, -1);
			joinTimes.put(pName, Utils.getUnixTimestamp(0L));

			// cancel out any previously delayed task created by this user
			if (this.playTimeLoadTasks.containsKey(pName)) {
				this.getServer().getScheduler().cancelTask(this.playTimeLoadTasks.get(pName));
			}
			
			// add new delayed task to load user's playtime from DB
			this.playTimeLoadTasks.put(pName, this.getServer().getScheduler().scheduleSyncDelayedTask(this, new Runnable() {
				@Override
				public void run() {
					// load playtimes for all players with time set to -1 and valid time on the server (i.e. +(minTimeFromLogout - 5) seconds)
					// ... and -5 seconds to allow for server lag and similar things to happen :-)
					Iterator<Entry<String, Integer>> it = CommandsEX.playTimes.entrySet().iterator();
					List<String> playerNames = new ArrayList<String>();
					Integer stamp = Utils.getUnixTimestamp(0L);
					while (it.hasNext()) {
						Map.Entry<String, Integer> pairs = (Map.Entry<String, Integer>)it.next();

						// only check players with -1 as playtime
						if (pairs.getValue() > -1) continue;

						// check if the player's last logout time was not within last minTimeFromLogout seconds, in which case we don't bother
						// checking up on him and his playtime will be loaded as needed on Quit event
						String pName = (String)pairs.getKey();
						OfflinePlayer o = CommandsEX.plugin.getServer().getOfflinePlayer(pName);
						
						if ((o != null) && (o.getLastPlayed() > 0)) {
							// convert miliseconds of player last visit time to seconds
							Integer lastPlay = Utils.getUnixTimestamp(o.getLastPlayed());
							// check if we should be adding the player, based on last quitting time
							if ((stamp - lastPlay) >= (CommandsEX.minTimeFromLogout - 5)) {
								playerNames.add(pName);
							}
						} else {
							// this is a new player, set his playtime to 0
							CommandsEX.playTimes.put(pName, 0);
						}
				        //it.remove(); // avoids a ConcurrentModificationException - not needed in our case and will clear out HashMap!
				    }
					
					// load playtimes from DB
					Integer pSize = playerNames.size();
					if (pSize > 0) {
						try {
							String[] qMarks = new String[pSize];
							Arrays.fill(qMarks, "?");
							ResultSet res = SQLManager.query_res("SELECT * FROM " + SQLManager.prefix + "playtime WHERE player_name IN ("+ Utils.implode(qMarks, ", ") +")", playerNames);
							while (res.next()) {
								CommandsEX.playTimes.put(res.getString("player_name"), res.getInt("seconds_played"));
							}
							res.close();
						} catch (Throwable e) {
							// unable to load players' playtimes, show up on console
							LogHelper.logSevere("[CommandsEX] " + _("dbTotalPlayTimeGetError", ""));
							LogHelper.logDebug("Message: " + e.getMessage() + ", cause: " + e.getCause());
							return;
						}
					}
				}
			}, (20 * minTimeFromLogout)));
		}
	}
  /**
   * * Give - Gives a player an item Could be improved in the future by adding enchantment support
   *
   * @param sender
   * @param args
   * @return
   */
  public static boolean run(CommandSender sender, String alias, String[] args) {

    if (sender instanceof Player) {
      Player player = (Player) sender;
      if (Utils.checkCommandSpam(player, "cex_give")) {
        return true;
      }
    }

    if (sender instanceof Player) {
      Player player = (Player) sender;
      if (Utils.checkCommandSpam(player, "cex_give")) {
        return true;
      }
    }

    if (args.length < 2 || args.length > 3) {
      System.out.println("Incorrect args");
      Commands.showCommandHelpAndUsage(sender, "cex_give", alias);
    } else {
      String item;
      short damage = 0;
      int amount = 64;

      Player target = Bukkit.getPlayer(args[0]);
      if (target == null) {
        LogHelper.showInfo("invalidPlayer", sender, ChatColor.RED);
        return true;
      }

      if (args[1].contains(":")) {
        String[] data = args[1].split(":");
        item = data[0];

        try {
          damage = Short.valueOf(args[1].split(":")[1]);
        } catch (Exception e) {
          LogHelper.showInfo("itemIncorrectDamageValue", sender, ChatColor.RED);
          Commands.showCommandHelpAndUsage(sender, "cex_give", alias);
          return true;
        }
      } else {
        item = args[1];
      }

      if (args.length == 3) {
        try {
          amount = Integer.valueOf(args[2]);
        } catch (Exception e) {
          LogHelper.showInfo("itemIncorrectDamageValue", sender, ChatColor.RED);
          Commands.showCommandHelpAndUsage(sender, "cex_give", alias);
          return true;
        }
      }

      if (Utils.closestMatches(item).size() > 0) {
        List<Material> matches = Utils.closestMatches(item);
        giveItem(sender, target, matches.get(0), amount, damage);
      } else {
        LogHelper.showInfo("itemNotFound", sender, ChatColor.RED);
      }
    }

    return true;
  }