/***
	 * 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", ""));
	}
	/***
	 * 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);
		}
	}
	/***
	 * 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();
	}