package fr.Alphart.BAT;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.TimeZone;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin;

import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;

import fr.Alphart.BAT.I18n.I18n;
import fr.Alphart.BAT.Modules.ModulesManager;
import fr.Alphart.BAT.Modules.Core.Core;
import fr.Alphart.BAT.Utils.CallbackUtils.Callback;
import fr.Alphart.BAT.Utils.RedisUtils;
import fr.Alphart.BAT.database.DataSourceHandler;

/**
 * Main class BungeeAdminTools
 * 
 * @author Alphart
 */
public class BAT extends Plugin {
	// This way we can check at runtime if the required BC build (or a higher one) is installed 
	private final int requiredBCBuild = 878;
	private static BAT instance;
	private static DataSourceHandler dsHandler;
	private Configuration config;
	private static String prefix;
	private ModulesManager modules;
	private RedisUtils redis;

	@Override
	public void onEnable() {
		instance = this;
		config = new Configuration();
		getLogger().setLevel(Level.INFO);
		if (!ProxyServer.getInstance().getName().equals("BungeeCord")) {
		  getLogger().warning("BungeeCord version check disabled because a fork has been detected."
              + " Make sur your fork is based on a BungeeCord build > #" + requiredBCBuild);
		}
		else if(getBCBuild() < requiredBCBuild && !new File(getDataFolder(), "skipversiontest").exists()){
		  getLogger().severe("Your BungeeCord build (#" + getBCBuild() + ") is not supported. Please use at least BungeeCord #" + requiredBCBuild);
		  getLogger().severe("If you want to skip that test, create a file named 'skipversiontest' in BAT directory.");
          getLogger().severe("BAT is going to shutdown ...");
          return;
		}
		if(config.isDebugMode()){
		    try{
		        final File debugFile = new File(getDataFolder(), "debug.log");
		        if(debugFile.exists()){
		            debugFile.delete();
		        }
		        // Write header into debug log
                Files.asCharSink(debugFile, Charsets.UTF_8).writeLines(Arrays.asList("BAT log debug file"
                        + " - If you have an error with BAT, you should post this file on BAT topic on spigotmc",
                        "Bungee build : " + ProxyServer.getInstance().getVersion(),
                        "BAT version : " + getDescription().getVersion(),
                        "Operating System : " + System.getProperty("os.name"),
                        "Timezone : " + TimeZone.getDefault().getID(),
                        "------------------------------------------------------------"));
		        final FileHandler handler = new FileHandler(debugFile.getAbsolutePath(), true);
		        handler.setFormatter(new Formatter() {
		            private final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
		            private final String pattern = "time [level] message\n";
                    @Override
                    public String format(LogRecord record) {
                        return pattern.replace("level", record.getLevel().getName())
                                    .replace("message", record.getMessage())
                                    .replace("[BungeeAdminTools]", "")
                                    .replace("time", sdf.format(Calendar.getInstance().getTime()));
                    }
                });
		        getLogger().addHandler(handler);
		        getLogger().setLevel(Level.CONFIG);
		        getLogger().info("The debug mode is now enabled ! Log are available in debug.log file located in BAT folder");
	            getLogger().config("Debug mode enabled ...");
	            getLogger().setUseParentHandlers(false);
		    }catch(final Exception e){
		        getLogger().log(Level.SEVERE, "An exception occured during the initialization of debug logging file", e);
		    }
		}
		prefix = config.getPrefix();
		loadDB(new Callback<Boolean>(){
			@Override
			public void done(final Boolean dbState, Throwable throwable) {
				if (dbState) {
				    getLogger().config("Connection to the database established");
					// Try enabling redis support.
					redis = new RedisUtils(config.isRedisSupport());
			        modules = new ModulesManager();
					modules.loadModules();
				} else {
					getLogger().severe("BAT is gonna shutdown because it can't connect to the database.");
					return;
				}
				// Init the I18n module
				I18n.getString("global");
			}
		});
	}
	
	public int getBCBuild(){
		final Pattern p = Pattern.compile(".*?:(.*?:){3}(\\d*)");
		final Matcher m = p.matcher(ProxyServer.getInstance().getVersion());
		int BCBuild;
		try{
			if (m.find()) {
			    BCBuild = Integer.parseInt(m.group(2));
			}else{
				throw new NumberFormatException();
			}
		}catch(final NumberFormatException e){
			// We can't determine BC build, just display a message, and set the build so it doesn't trigger the security
			getLogger().info("BC build can't be detected. If you encounter any problems, please report that message. Otherwise don't take into account");
			BCBuild = requiredBCBuild;
		}
		return BCBuild;
	}

	@Override
	public void onDisable() {
	    if(redis != null){
	      getRedis().destroy();
	    }
        modules.unloadModules();
		instance = null;
	}

	public void loadDB(final Callback<Boolean> dbState) {
		if (config.isMysql_enabled()) {
		    getLogger().config("Starting connection to the mysql database ...");
			final String username = config.getMysql_user();
			final String password = config.getMysql_password();
			final String database = config.getMysql_database();
			final String port = config.getMysql_port();
			final String host = config.getMysql_host();
			// BoneCP can accept no database and we want to avoid that
			Preconditions.checkArgument(!"".equals(database), "You must set the database.");
			ProxyServer.getInstance().getScheduler().runAsync(this, new Runnable() {
				@Override
				public void run() {
				    try{
				        dsHandler = new DataSourceHandler(host, port, database, username, password);
	                    final Connection c = dsHandler.getConnection();
	                    if (c != null) {
                            c.close();
                            dbState.done(true, null);
                            return;
	                    }
				    }catch(final SQLException handledByDatasourceHandler){}
				    getLogger().severe("The connection pool (database connection)"
                            + " wasn't able to be launched !");
                    dbState.done(false, null);
				}
			});
		}
		// If MySQL is disabled, we are gonna use SQLite
		// Before initialize the connection, we must download the sqlite driver
		// (if it isn't already in the lib folder) and load it
		else {
		    getLogger().config("Starting connection to the sqlite database ...");
			getLogger().warning("It is strongly DISRECOMMENDED to use SQLite with BAT,"
					+ " as the SQLite implementation is less stable and much slower than the MySQL implementation.");
			if(loadSQLiteDriver()){
				dsHandler = new DataSourceHandler();
				dbState.done(true, null);
			}else{
				dbState.done(false, null);
			}
		}
	}

	public boolean loadSQLiteDriver(){
		final File driverPath = new File(getDataFolder() + File.separator + "lib" + File.separator
				+ "sqlite_driver.jar");
		new File(getDataFolder() + File.separator + "lib").mkdir();

		// Download the driver if it doesn't exist
		if (!new File(getDataFolder() + File.separator + "lib" + File.separator + "sqlite_driver.jar").exists()) {
			getLogger().info("The SQLLite driver was not found. It is being downloaded, please wait ...");

			final String driverUrl = "https://www.dropbox.com/s/ls7qoddx9m6t4vh/sqlite_driver.jar?dl=1";
			FileOutputStream fos = null;
			try {
				final ReadableByteChannel rbc = Channels.newChannel(new URL(driverUrl).openStream());
				fos = new FileOutputStream(driverPath);
				fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
			} catch (final IOException e) {
				getLogger()
				.severe("An error occured during the downloading of the SQLite driver. Please report this error : ");
				e.printStackTrace();
				return false;
			} finally {
				DataSourceHandler.close(fos);
			}

			getLogger().info("The driver has been successfully downloaded.");
		}

		// Load the driver
		try {
			URLClassLoader systemClassLoader;
			URL u;
			Class<URLClassLoader> sysclass;
			u = driverPath.toURI().toURL();
			systemClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
			sysclass = URLClassLoader.class;
			final Method method = sysclass.getDeclaredMethod("addURL", new Class[] { URL.class });
			method.setAccessible(true);
			method.invoke(systemClassLoader, new Object[] { u });

			Class.forName("org.sqlite.JDBC");
			return true;
		} catch (final Throwable t) {
			getLogger().severe("The sqlite driver cannot be loaded. Please report this error : ");
			t.printStackTrace();
			return false;
		}
	}

	public static BAT getInstance() {
		return BAT.instance;
	}

	public static BaseComponent[] __(final String message) {
		return TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', prefix + message));
	}

	/**
	 * Send a broadcast message to everyone with the given perm <br>
	 * Also broadcast through Redis if it's installed that's why this method <strong>should not be called
	 * from a Redis call</strong> otherwise it will broadcast it again and again
	 * @param message
	 * @param perm
	 */
	public static void broadcast(final String message, final String perm) {
		noRedisBroadcast(message, perm);
		if(BAT.getInstance().getRedis().isRedisEnabled()){
			BAT.getInstance().getRedis().sendBroadcast(perm, message);
		}
	}
	
	public static void noRedisBroadcast(final String message, final String perm) {
		final BaseComponent[] bsMsg = __(message);
		for (final ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
			if (p.hasPermission(perm) || p.hasPermission("bat.admin")) {
				p.sendMessage(bsMsg);
			}
			// If he has a grantall permission, he will have the broadcast on all the servers
			else{
				for(final String playerPerm : Core.getCommandSenderPermission(p)){
					if(playerPerm.startsWith("bat.grantall.")){
						p.sendMessage(bsMsg);
						break;
					}
				}
			}
		}
		getInstance().getLogger().info(ChatColor.translateAlternateColorCodes('&', message));
	}

	public ModulesManager getModules() {
		return modules;
	}

	public Configuration getConfiguration() {
		return config;
	}

	public static Connection getConnection() {
		return dsHandler.getConnection();
	}

	public DataSourceHandler getDsHandler() {
		return dsHandler;
	}

	public RedisUtils getRedis() {
	    return redis;
	}

	/**
	 * Kick a player from the proxy for a specified reason
	 * 
	 * @param player
	 * @param reason
	 */
	public static void kick(final ProxiedPlayer player, final String reason) {
		if (reason == null || reason.equals("")) {
			player.disconnect(TextComponent.fromLegacyText("You have been disconnected of the server."));
		} else {
			player.disconnect(TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', reason)));
		}
	}

}