package fr.Alphart.BAT.database;

import static java.lang.String.format;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import com.zaxxer.hikari.HikariDataSource;
import net.md_5.bungee.api.ProxyServer;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.varia.NullAppender;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.io.CharStreams;
import com.mysql.jdbc.exceptions.jdbc4.CommunicationsException;

import fr.Alphart.BAT.BAT;
import fr.Alphart.BAT.Utils.CallbackUtils.Callback;

public class DataSourceHandler {
	// Connection informations
	private HikariDataSource ds;
	private String username;
	private String password;
	private String database;
	private String port;
	private String host;
	
	private static boolean sqlite = false; // If sqlite is used or not
	private Connection SQLiteConn;

	/**
	 * Constructor used for MySQL
	 * 
	 * @param host
	 * @param port
	 * @param database
	 * @param username
	 * @param password
	 * @throws SQLException 
	 */
	public DataSourceHandler(final String host, final String port, final String database, final String username, final String password) throws SQLException{
		// Check database's informations and init connection
		this.host = Preconditions.checkNotNull(host);
		this.port = Preconditions.checkNotNull(port);
		this.database = Preconditions.checkNotNull(database);
		this.username = Preconditions.checkNotNull(username);
		this.password = Preconditions.checkNotNull(password);

		BAT.getInstance().getLogger().config("Initialization of HikariCP in progress ...");
		BasicConfigurator.configure(new NullAppender());
		ds = new HikariDataSource();
		ds.setJdbcUrl("jdbc:mysql://" + this.host + ":" + this.port + "/" + this.database + 
				"?useLegacyDatetimeCode=false&serverTimezone=" + TimeZone.getDefault().getID());
		ds.setUsername(this.username);
		ds.setPassword(this.password);
		ds.addDataSourceProperty("cachePrepStmts", "true");
		ds.setMaximumPoolSize(8);
		try {
			final Connection conn = ds.getConnection();
		    int intOffset = Calendar.getInstance().getTimeZone().getOffset(Calendar.getInstance().getTimeInMillis()) / 1000;
		    String offset = String.format("%02d:%02d", Math.abs(intOffset / 3600), Math.abs((intOffset / 60) % 60));
		    offset = (intOffset >= 0 ? "+" : "-") + offset;
			conn.createStatement().executeQuery("SET time_zone='" + offset + "';");
			conn.close();
			BAT.getInstance().getLogger().config("BoneCP is loaded !");
		} catch (final SQLException e) {
			BAT.getInstance().getLogger().severe("BAT encounters a problem during the initialization of the database connection."
					+ " Please check your logins and database configuration.");
			if(e.getCause() instanceof CommunicationsException){
			    BAT.getInstance().getLogger().severe(e.getCause().getMessage());
			}
			if(BAT.getInstance().getConfiguration().isDebugMode()){
			    BAT.getInstance().getLogger().log(Level.SEVERE, e.getMessage(), e);
			}
			throw e;
		}
		sqlite = false;
	}

	/**
	 * Constructor used for SQLite
	 */
	public DataSourceHandler() {
		/*
		 * As SQLite supports concurrency pretty badly (locked database which causes problem), we're gonna get a connection from the DriverManager each time
		 * we need to acces to the database. In the contrary of BoneCP with mysql in which we saved connection to optimize perfomance, it's not necessary with SQLite.
		 * FYI, here are the results of test : execute 1000 insert request using SQLite, with or without using the same connection :
		 * - Using the same connection it took : 22820 ms
		 * - Getting another connection each time (DriverManager.getConnection), it took : 24186 ms
		 * The difference is only 1366 ms for 1000 request, that means on average additional 1.3 ms, which is insignificant as we are executing almost every query async.
		 * To the people who read that, all these calculations can seem a little overrated, but I really like to improve perfomance at the most and I'm pretty curious :p
		 */
		sqlite = true;
		try {
			SQLiteConn = DriverManager.getConnection("jdbc:sqlite:" + BAT.getInstance().getDataFolder().getAbsolutePath() + File.separator
					+ "bat_database.db");
			SQLiteConn.close();
		} catch (SQLException e) {
			BAT.getInstance().getLogger().severe("BAT encounters a problem during the initialization of the sqlite database connection.");
			if(e.getMessage() != null){
				BAT.getInstance().getLogger().severe("Error message : " + e.getMessage());
			}
		}
	}

	public Connection getConnection() {
		try {
			if(sqlite){
				// To avoid concurrency problem with SQLite, we will just use one connection. Cf : constructor above for SQLite
				synchronized (SQLiteConn) {
					SQLiteConn = DriverManager.getConnection("jdbc:sqlite:" + BAT.getInstance().getDataFolder().getAbsolutePath() + File.separator
								+ "bat_database.db");
					return SQLiteConn;
				}
			}
			return ds.getConnection();
		} catch (final SQLException e) {
			BAT.getInstance().getLogger().severe(
			        "BAT can't etablish connection with the database. Please report this and include the following lines :");
			if(e.getCause() instanceof CommunicationsException){
			    BAT.getInstance().getLogger().severe(e.getCause().getMessage());
			}
            if (BAT.getInstance().getConfiguration().isDebugMode()) {
                e.printStackTrace();
            }
			return null;
		}
	}

	public boolean getSQLite() {
		return sqlite;
	}
	
	public static boolean isSQLite() {
		return sqlite;
	}

	/**
	 * Generate a backup of the BAT data in mysql database.
	 * @param path
	 * @param onComplete
	 * @throws RuntimeException if MySQL is not used or if the creation of the backup file failed
	 */
	public void generateMysqlBackup(final Callback<String> onComplete) throws RuntimeException{
		ProxyServer.getInstance().getScheduler().runAsync(BAT.getInstance(), new Runnable(){
			@Override
			public void run() {
				try {
					Process testProcess = Runtime.getRuntime().exec("mysqldump --help");
					new StreamPumper(testProcess.getErrorStream()).pump();
					new StreamPumper(testProcess.getInputStream()).pump();
					int returnValue = testProcess.waitFor();
					if(returnValue != 0){
					    throw new Exception();
					}
				} catch (final Exception e) {
					onComplete.done("The backup can't be achieved because mysqldump is nowhere to be found.", null);
					return;
				}
				final File backupDirectory = new File(BAT.getInstance().getDataFolder().getAbsolutePath() 
						+ File.separator + "databaseBackups");
				backupDirectory.mkdir();
				File backupFile = new File(backupDirectory.getAbsolutePath() + File.separator + "backup" +
						new SimpleDateFormat("dd-MMM-yyyy_HH'h'mm").format(Calendar.getInstance().getTime()) + ".sql");
				for(int i = 0;;i++){
					if(!backupFile.exists()){
						break;
					}else{
						if(i == 0){
							backupFile = new File(backupFile.getAbsolutePath().replace(".sql",  "#" + i + ".sql"));
						}
						else{
							backupFile = new File(backupFile.getAbsolutePath().replaceAll("#\\d+\\.sql$", "#" + i + ".sql"));
						}
					}
				}
				String backupCmd = "mysqldump -u {user} -p --add-drop-database -r {path} {database} {tables}";
				final String tables = Joiner.on(' ').join(Arrays.asList(SQLQueries.Ban.table, SQLQueries.Mute.table,
						SQLQueries.Kick.table, SQLQueries.Comments.table, SQLQueries.Core.table));
				String backupPath = backupFile.getAbsolutePath();
				if(backupPath.contains(" ")){
				    backupPath = "\"" + backupPath + "\"";
				}
				backupCmd = backupCmd.replace("{user}", username).replace("{database}", database)
						.replace("{path}", backupPath).replace("{tables}", tables);
				if(password.equals("")){
					backupCmd = backupCmd.replace("-p", "");
				}else{
					backupCmd = backupCmd.replace("-p", "--password=" + password);
				}
				try {
					Process backupProcess = Runtime.getRuntime().exec(backupCmd);
	                final StreamPumper errorPumper = new StreamPumper(backupProcess.getErrorStream());
	                errorPumper.pump();
	                new StreamPumper(backupProcess.getInputStream()).pump();;
					int exitValue = backupProcess.waitFor();
					if(exitValue == 0){
						final String[] splittedPath = backupFile.getAbsolutePath().split((File.separator.equals("\\") ? "\\\\" : File.separator));
						final String fileName = splittedPath[splittedPath.length - 1];
						onComplete.done(format("The backup file (%s) has been sucessfully generated.", fileName), null);
					}else{
						onComplete.done("An error happens during the creation of the mysql backup. Please check the logs", null);
						BAT.getInstance().getLogger().severe("An error happens during the creation of the mysql backup. Please report :");
						for(final String message : errorPumper.getLines()){
						    BAT.getInstance().getLogger().severe(message);
						}
					}
				} catch (final Exception e) {
					onComplete.done("An error happens during the creation of the mysql backup.", e);
					e.printStackTrace();
				}
			}
		});
	}
	
	// Useful methods
	public static String handleException(final SQLException e) {
		BAT.getInstance()
		.getLogger()
		.severe("BAT encounters a problem with the database. Please report this and include the following lines :");
		e.printStackTrace();
		return "An error related to the database occured. Please check the log.";
	}

	public static void close(final AutoCloseable... closableList) {
		for (final AutoCloseable closable : closableList) {
			if (closable != null) {
				try {
					closable.close();
				} catch (final Throwable ignored) {
				}
			}
		}
	}
	
	public class StreamPumper{
	    private final InputStreamReader reader;
	    private List<String> pumpedLines = null;
	    
	    public StreamPumper(final InputStream is){
	        reader = new InputStreamReader(is);
	    }
	    
	    /**
	     * Starts a new async task and pump the inputstream
	     */
	    public void pump(){
	        ProxyServer.getInstance().getScheduler().runAsync(BAT.getInstance(), new Runnable() {
                @Override
                public void run() {
                    try {
                        pumpedLines = CharStreams.readLines(reader);
                        reader.close();
                    } catch (final IOException e) {
                        BAT.getInstance().getLogger().severe("BAT encounter an error while reading the stream of subprocess. Please report this :");
                        e.printStackTrace();
                    }
                }
            });
	    }
	    
	    public List<String> getLines(){
	        if(pumpedLines == null){
	            return new ArrayList<String>();
	        }
	        return pumpedLines;
	    }
	}
}