/*
 * @Author Lionel Conforto, 2008-2011
 * @History
 * 19 Sep 2011: corrected error in support of semicolon (;) within sql statements
 * 19 Sep 2011: added support for SQL comments. Lines that start with double - will be ignored
 */
package com.projectsontracks.controller;

import static com.projectsontracks.controller.ConsoleCommand.cmdLineConsole;
import com.projectsontracks.model.BroadSQLException;
import com.projectsontracks.model.SQLDatabase;
import com.projectsontracks.model.Session;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import org.apache.commons.lang3.StringUtils;

/**
 *
 * @author lconfort
 */
public class ConsoleManager {

    public final static String CMD_CORE = "Core";
    public final static String CMD_EXTENSION = "Extension";
    private String platform = null;
    private String platformsFileName = null;
    private char separator = ConsoleSettings.defaultSeparator;
    private boolean extractMode = false; // Should we extract to a file : true/false
    private String extractFileName = null;  // File name where we extract results
    private boolean extractAppendToFile = false; // If extract to a file, should we overwrite or append. False means overwrite.
    private boolean showToScreen = true;
    private boolean listMode = false;
    private String query = null;
    private String lastSQLQuery = null;
    //private String qryFileName = null; // for executing queries stored in a file
    private static final String CONS_TERM = ConsoleSettings.CMD_EXIT;
    private String rootFolder = ConsoleSettings.extractFolderName;
    //private String rootFileName = ConsoleSettings.extractDefaultFileName;
    private boolean printToLogFile = false; // copies the console input/output to a log file
    private boolean debugMode = false;
    private Map<String, Class> commands = new HashMap<String, Class>();
    private static ConsoleCmdLineGUI console = null;
    private static Session session = null;
    private static SQLDatabase currentDatabase = null;

    public ConsoleManager() {
    }

    public ConsoleManager(String platform, String pFName, Session sess, ConsoleCmdLineGUI cns) {
        this.session = sess;
        this.console = cns;
        this.platform = platform;
        this.platformsFileName = pFName;
        console.setPrompt(this.platform + "> ");
        this.printToLogFile = ConsoleSettings.logDefaultActivated;
        console.setPrintToLogFile(this.printToLogFile);
    }

    public boolean isExtractMode() {
        return extractMode;
    }

    public void setExtractMode(boolean extractMode) {
        this.extractMode = extractMode;
    }

    public String getPlatform() {
        return platform;
    }

    public void setPlatform(String platform) {
        this.platform = platform;
    }

    public String getPlatformsFileName() {
        return platformsFileName;
    }

    public void setPlatformsFileName(String platformsFileName) {
        this.platformsFileName = platformsFileName;
    }

    public String getRootFolder() {
        return rootFolder;
    }

    public void setRootFolder(String rootFolder) {
        this.rootFolder = rootFolder;
    }

    public char getSeparator() {
        return separator;
    }

    public void setSeparator(char separator) {
        this.separator = separator;
    }

    /*
     * Retrieves all classes that extend ConsoleCommand
     * These classes are searched in 2 locations:
     *    1) com.projectsontracks.controller
     *    2) com.projectsontracks.extensions
     *    3) Custom extensions if the folder exists
     *  If you want to create your own custom classes, simply create a new class in
     *  folder extensions.
     * @return a map of classes names grouped in core and extension classes
     * @throws CliSQLException
     * 
     */
    public static HashMap<String, String> loadAvailableCommands() throws BroadSQLException {
        HashMap<String, String> classes = new HashMap<String, String>();
        Class motherClass = ConsoleCommand.class;
        try {

            // Core lclCommands
            Class[] tClasses = getClassesInJarFile(ConsoleSettings.defaultCliSQLJarFile, ConsoleSettings.packageNameDefault);
            for (Class tClass : tClasses) {
                if (motherClass != tClass && motherClass.isAssignableFrom(tClass)) {
                    classes.put(tClass.getCanonicalName(), CMD_CORE);
                }
            }

            // Core Extensions
            //Class[] eClasses = getClasses(ConsoleSettings.packageNameExtensions);
            Class[] eClasses = getClassesInJarFile(ConsoleSettings.defaultCliSQLJarFile, ConsoleSettings.packageNameExtensions);
            for (Class tClass : eClasses) {
                if (motherClass != tClass && motherClass.isAssignableFrom(tClass)) {
                    classes.put(tClass.getCanonicalName(), CMD_EXTENSION);
                }
            }

            // User Extensions
            if (ConsoleSettings.packageCustomExtensions != null && !"".equals(ConsoleSettings.packageCustomExtensions.trim())) {
                //System.out.println("Loading custom lclCommands ...");
                //Class[] uClasses = getClasses(ConsoleSettings.packageCustomExtensions);
                //System.out.println("Classpath: " + System.getProperty("java.classpath"));
                //System.out.println("Custom JAR file: "+ConsoleSettings.customJarFile);
                //System.out.println("Custom Extensions: "+ConsoleSettings.packageCustomExtensions);
                Class[] uClasses = getClassesInJarFile(ConsoleSettings.customJarFile, ConsoleSettings.packageCustomExtensions);
                for (Class tClass : uClasses) {
                    //System.out.println(tClass.getName());
                    if (motherClass != tClass && motherClass.isAssignableFrom(tClass)) {
                        classes.put(tClass.getCanonicalName(), CMD_EXTENSION);
                    }
                }
                if (uClasses == null || uClasses.length <= 0) {
                    console.println("No custom extensions found for parameter CustomExtensionsPackage=" + ConsoleSettings.packageCustomExtensions, ConsoleCmdLineGUI.MSG_INFO, true);
                }
            }
        } catch (ClassNotFoundException | IOException ex) {
            throw new BroadSQLException(ex);
        }
        return (classes);
    }//registerClassNames

    /**
     * Recursive method used to find all classes in a given directory and sub
     * directories
     *
     * @param directory The base directory
     * @param packageName The package name for classes found inside the base
     * directory
     * @return a list of classes in the directory
     * @throws ClassNotFoundException
     */
    private static List<Class> findClasses(File directory, String packageName) throws ClassNotFoundException {
        List<Class> classes = new ArrayList<Class>();
        if (!directory.exists()) {
            return classes;
        }
        File[] files = directory.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                assert !file.getName().contains(".");
                classes.addAll(findClasses(file, packageName + "." + file.getName()));
            } else if (file.getName().endsWith(".class")) {
                classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
            }
        }
        return classes;
    }//findClasses

    private static void extractClassFromJar(final String jar, final String packageName, final List classes, JarEntry jarEntry) throws BroadSQLException {
        String className = jarEntry.getName();
        //System.out.println(className);
        if (className.endsWith(".class")) {
            className = className.substring(0, className.length() - ".class".length());
            //System.out.println(className);
            if (packageName == null || className.startsWith(packageName)) {
                //System.out.println("*** " + className);                
                try {
                    classes.add(Class.forName(className.replace('/', '.')));
                } catch (ClassNotFoundException cnfe) {
                    String msg = "extractClassFromJar: unable to find class named " + className.replace('/', '.') + "' within jar '" + jar + "'";
                    System.out.println(msg);
                    throw new BroadSQLException(cnfe);
                }
            }
        }
    }

    private static void closeJarFile(final JarInputStream jarFile) throws BroadSQLException {
        if (jarFile != null) {
            try {
                jarFile.close();
            } catch (IOException ioe) {
                throw new BroadSQLException(ioe);
            }
        }
    }

    /**
     * Recursive method used to find all classes in a JAR file
     *
     * @param directory The base directory
     * @param packageName The package name for classes found inside the base
     * directory
     * @return a list of classes in the directory
     * @throws ClassNotFoundException
     */
    private static List<Class> findClassesInJarFile(String jar, String packageName) throws ClassNotFoundException, BroadSQLException {
        List<Class> classes = new ArrayList<Class>();
        JarInputStream jarFile = null;
        try {
            jarFile = new JarInputStream(new FileInputStream(jar));
            JarEntry jarEntry;
            do {
                try {
                    jarEntry = jarFile.getNextJarEntry();
                    if (jarEntry != null) {
                        extractClassFromJar(jar, packageName, classes, jarEntry);
                    }
                } catch (IOException ioe) {
                    String msg = "findClassesInJarFile: Unable to get next jar entry from jar file '" + jar + "'";
                    System.out.println(msg);
                    throw new BroadSQLException(ioe);
                }
            } while (jarEntry != null);
            closeJarFile(jarFile);
        } catch (IOException ioe) {
            String msg = "Unable to retrieve custom extensions from '" + jar + "'";
            if (console != null) {
                console.println(msg, ConsoleCmdLineGUI.MSG_CRIT, true);
                console.println("");
            } else {
                System.out.println(msg);
            }
            //throw new CliSQLException(ioe);
        } finally {
            closeJarFile(jarFile);
        }
        return classes;
    }//findClasses

    private static Class[] getClasses(String packageName) throws ClassNotFoundException, IOException {
        //System.out.println("Hello ...");
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        assert classLoader != null;
        String path = packageName.replace('.', '/');
        //System.out.println("Package: "+path);
        Enumeration<URL> resources = classLoader.getResources(path);
        //System.out.println("Path="+path);
        List<File> dirs = new ArrayList<File>();
        while (resources.hasMoreElements()) {
            URL resource = resources.nextElement();
            dirs.add(new File(resource.getFile()));
            //System.out.println("File: "+ new File(resource.getFile()).getName());
        }
        ArrayList<Class> classes = new ArrayList<Class>();
        for (File directory : dirs) {
            //System.out.println(directory.getName());
            classes.addAll(findClasses(directory, packageName));
        }
        return classes.toArray(new Class[classes.size()]);
    }//getClasses

    private static Class[] getClassesInJarFile(String jarFile, String packageName) throws ClassNotFoundException, IOException, BroadSQLException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        assert classLoader != null;
        ArrayList<Class> classes = new ArrayList<Class>();
        classes.addAll(findClassesInJarFile(jarFile, packageName));
        return classes.toArray(new Class[classes.size()]);
    }//getClassesInJar

    private Class getCommandFromName(String uQuery) {
        Class tClass = null;
        if (commands != null && !commands.isEmpty()) {
            for (String cmdName : commands.keySet()) {
                if (uQuery.startsWith(cmdName)) {
                    tClass = (Class) commands.get(cmdName);
                    break;
                }
            }//for
        }
        return (tClass);
    }//getCommandFromName

    private Map<String, Class> getConsoleCommandsRevised() throws BroadSQLException {
        Map<String, Class> map = new HashMap<String, Class>();
        HashMap<String, String> classMap = loadAvailableCommands();
        Set<String> classes = classMap.keySet();
        for (String className : classes) {
            try {
                Class classObject = Class.forName(className);
                try {
                    ConsoleCommand cmd = (ConsoleCommand) classObject.newInstance();
                    if (cmd != null) {
                        String[] keyWords = cmd.getKeywords();
                        for (String keyWord : keyWords) {
                            map.put(keyWord, classObject);
                        }
                    }
                } catch (InstantiationException ex) {
                    console.error(new BroadSQLException(ex));
                } catch (IllegalAccessException ex) {
                    console.error(new BroadSQLException(ex));
                }

            } catch (ClassNotFoundException ex) {
                throw new BroadSQLException(ex);
            }
        }
        return (map);
    }//getConsoleCommands

    /*
     * Replace macro lclCommands with their corresponding SQL code
     */
    public static String substituteMacros(String query) {
        String result = query;
        /* List of values, stored in a text file
         * command: <@ >
         * Reads values in a file and returns a list like ('A','B','C')
         */
        while (result.contains("<@") && result.contains(">")) {
            //String fileName = StringUtils.substringBefore(StringUtils.substringAfter(result, "<@"),">");            
            String fileName = StringUtils.substringBetween(result, "<@", ">").trim();
            StringBuilder codes = new StringBuilder();
            try {
                FileReader fr = new FileReader(fileName);
                BufferedReader br = new BufferedReader(fr);
                String line = null;
                codes.append("(");
                int i = 0;
                while ((line = br.readLine()) != null) {
                    if (!"".equals(line.trim())) {
                        line = "'" + line.trim() + "'";
                        if (i > 0) {
                            codes.append(",");
                        }
                        codes.append(line);
                        i++;
                    }
                }
                codes.append(")");
                fr.close();
            } catch (FileNotFoundException fnfe) {
                console.println("File " + fileName + " not found");
            } catch (IOException ie) {
                console.println("Error was encountered when opening file " + fileName);
            }
            String listOfCodes = codes.toString();
            result = StringUtils.replace(result, "<@" + fileName + ">", listOfCodes);

        }//while

        return (result);
    }//substituteMacros

    /*
     * Replace macro lclCommands with their corresponding SQL code
     */
    public static String substituteMacros_V100(String query) {
        String result = query;

        /* List of values, stored in a text file
         * command: <@ >
         * Reads values in a file and returns a list like ('A','B','C')
         */
        if (result.contains("<@") && result.contains(">")) {
            //String fileName = StringUtils.substringBefore(StringUtils.substringAfter(result, "<@"),">");            
            String fileName = StringUtils.substringBetween(result, "<@", ">").trim();
            StringBuilder codes = new StringBuilder();
            try {
                FileReader fr = new FileReader(fileName);
                BufferedReader br = new BufferedReader(fr);
                String line = null;
                codes.append("(");
                int i = 0;
                while ((line = br.readLine()) != null) {
                    if (!"".equals(line.trim())) {
                        line = "'" + line.trim() + "'";
                        if (i > 0) {
                            codes.append(",");
                        }
                        codes.append(line);
                        i++;
                    }
                }
                codes.append(")");
                fr.close();
            } catch (FileNotFoundException fnfe) {
                console.println("File " + fileName + " not found");
            } catch (IOException ie) {
                console.println("Error was encountered when opening file " + fileName);
            }
            String listOfCodes = codes.toString();
            result = StringUtils.replace(result, "<@" + fileName + ">", listOfCodes);

        }

        return (result);
    }//substituteMacros_V100

    public void executeCommand() throws BroadSQLException {

        if (query != null) {
            // First check that we're still connected
            if (currentDatabase == null || !currentDatabase.isConnected()) {
                currentDatabase.connect();
            }

            // Execute the command
            String uQuery = query.toUpperCase();
            commands = getConsoleCommandsRevised();
            Class tClass = getCommandFromName(uQuery);

            if (tClass == null && ConsoleSettings.processUnknownCommands == true) {
                // We must execute default command !!
                tClass = commands.get(ConsoleSettings.defaultCommandName);
            }

            if (tClass != null) {
                //console.println("Executing command: " + tClass.getCanonicalName());
                try {

                    // ANTE EXECUTION of the command
                    // *********************************************
                    ConsoleCommand command = null;
                    command.setCmdLineConsole(console);
                    command.setSession(session);
                    command = (ConsoleCommand) tClass.newInstance();
                    command.setDatabase(currentDatabase);
                    command.setPlatform(this.platform);
                    command.setDebugMode(this.debugMode);
                    command.setPrintToLogFile(printToLogFile);
                    command.setListMode(listMode);
                    command.setPlatformsFileName(this.platformsFileName);
                    command.setExtractMode(this.extractMode);
                    if (this.extractMode) {
                        command.setExtractFileName(this.extractFileName);
                        command.setExtractAppendToFile(this.extractAppendToFile);
                    } else {
                        command.setExtractFileName(null);
                    }
                    //System.out.println("Sep(beofre)="+this.separator);
                    command.setSeparator(this.separator);
                    command.setLastSQLQuery(lastSQLQuery);
                    //System.out.println("File="+this.extractFileName); 
                    ConsoleCmdLineGUI.setWindowsTitle(this.platform, this.query, false);
                    // Substitute macro lclCommands if necessary                        
                    this.query = substituteMacros(this.query);
                    // End Block ANTE EXECUTION

                    // ACTUAL EXECUTION of the command
                    // *********************************************
                    command.execute(this.query);

                    // POST EXECUTION of the command
                    // *********************************************
                    currentDatabase = command.getDatabase();
                    this.platform = command.getPlatform();
                    ConsoleCmdLineGUI.setWindowsTitle(this.platform, "", true);
                    this.debugMode = command.isDebugMode();
                    this.printToLogFile = command.isPrintToLogFile();
                    console.setPrintToLogFile(this.printToLogFile);
                    this.listMode = command.isListMode();
                    this.extractFileName = command.getExtractFileName();
                    this.extractMode = command.isExtractMode();
                    if (this.extractMode) {
                        // After user specified the file for data export
                        this.showToScreen = false;
                        if (this.extractFileName != null) {
                            currentDatabase.setFileName(extractFileName);
                        }
                    } else {
                        // After user executed actual export to external file
                        this.showToScreen = true;
                        currentDatabase.setFileName(null);
                    }
                    //System.out.println("File name: " + currentDatabase.getFileName());
                    this.extractAppendToFile = false; // by default we don't append
                    this.separator = command.getSeparator();
                    this.lastSQLQuery = command.getLastSQLQuery();
                    // End Block POST EXECUTION                        

                } catch (IllegalAccessException | InstantiationException ie) {
                    throw new BroadSQLException(ie);
                }
            } else {
                console.error("Unknown Command: " + query);
            }

        }//console
    }//executeCommandRevised

    public static void attachShutDownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                //console.println("");
                //console.println("ConsoleManager: Unexpected termination: closing open connection");
                try {
                    if (currentDatabase != null && currentDatabase.isConnected()) {
                        currentDatabase.close();
                    }
                } catch (BroadSQLException se) {
                    se.printStackTrace();
                }
            }
        });
        //System.out.println("Shut Down Hook Attached.");
    }//attachShutDownHook

    private void executeUserScripts() throws BroadSQLException { 
        TreeSet<String> userCmds = session.getCurrentUserLoginScript(platform, currentDatabase);
        if (userCmds != null && !userCmds.isEmpty()) {
            console.println("Running user's login commands ...");
            for (String command : userCmds) {
                console.println(command);
                this.query = command;
                executeCommand();
            }
            console.println("... user's login commands done");
        }
    }//executeUserScripts

    /*
     * Reads input from the console until a line that ends with a semicolon (;) is entered
     * In that case, all the entered lines are assembled into a query and sent to the
     * function executeCommand(String query) for execution
     * @author Lionel Conforto, 5 May 2011
     */
    public void run() throws BroadSQLException {
        String line = null;
        boolean cmdExecuted = false;
        attachShutDownHook();

        // Test the connection
        SQLDatabase dbTest = null;
        try {
            dbTest = new SQLDatabase(platform, false, null);
        } catch (BroadSQLException se) {
            platform = ConsoleSettings.cdfId;
            dbTest = new SQLDatabase(platform, false, null);
            ConsoleCmdLineGUI.setPrompt(this.platform + "> ");
        }
        StringBuffer bsTest = dbTest.testConnection(platform);
        console.writeln("Connected to " + platform + " using JDBC driver: " + dbTest.getDbDriver());
        if (dbTest.getDbName().equalsIgnoreCase("Oracle")) {
            console.writeln(dbTest.getDbVersion());
            console.println("");
        } else {
            console.writeln("Database: " + dbTest.getDbName() + " " + dbTest.getDbVersion());
            console.println("");
        }

        // Opens the database with current platform
        currentDatabase = session.openDatabase(platform);
        currentDatabase.setCmdLineConsole(console);
        if (currentDatabase.isConnected()) {
            // Sets default settings
            currentDatabase.setMaxRowsOnScreen(ConsoleSettings.maxRowsOnScreen);
            currentDatabase.setSep(this.separator);
            currentDatabase.setToScreen(this.showToScreen);
            currentDatabase.setMaxRowsOnScreen(ConsoleSettings.maxRowsOnScreen);
            if ("FALSE".equalsIgnoreCase(ConsoleSettings.autoCommit)) {
                currentDatabase.setAutoCommit(false);
            } else {
                currentDatabase.setAutoCommit(true);
            }

            // Executes login scripts
            executeUserScripts();


            // Pour la déconnexion automatique
            StringBuilder bs = new StringBuilder();
            while ((line = console.readLine()) != null && !line.startsWith(CONS_TERM)) {
                //System.out.println("Is connected? "+currentDatabase.isConnected());
                if (!session.isActive()) {
                    session.authenticate(console);
                }

                if (this.printToLogFile) {
                    try {
                        ConsoleLogger.writeln(console.getPrompt() + line);
                    } catch (IOException ex) {
                        this.printToLogFile = false;
                        console.error("Unable to write to log file");
                    }
                }
                cmdExecuted = false;
                line = line.trim();

                if ("/".equals(line) || (line.startsWith("/") && !line.startsWith("/*"))) {
                    if (this.lastSQLQuery != null && !"".equals(this.lastSQLQuery.trim())) {
                        this.query = this.lastSQLQuery;
                        executeCommand();
                        cmdExecuted = true;
                    } else {
                        console.println("No command in memory\n");
                    }

                    // Lines ending with \; are not considered as termination of command. Semicolon is escaped    
                } else if (line.endsWith("\\;")) {
                    if (!line.startsWith("--")) {
                        bs.append(StringUtils.substringBefore(line, "\\;"));
                        bs.append(";");
                        bs.append(" ");
                    }

                } else if (line.endsWith(";") && !line.endsWith("\\;")) {
                    // Execution of the command entered in GUI
                    // =======================================
                    if (line.startsWith("@")) {
                        // Execute lclCommands from an external file
                        // ======================================
                        String qryFileName = StringUtils.substringAfterLast(line, "@");
                        if (qryFileName.endsWith(";")) {
                            qryFileName = StringUtils.substringBefore(qryFileName, ";");
                        }
                        Queries queries = new Queries(qryFileName);
                        try {
                            queries.load();
                            if (queries != null && !queries.isEmpty()) {
                                for (Query oQuery : queries) {
                                    String qry = oQuery.getQuery();
                                    //System.out.println("DB1: "+qry);
                                    if (qry.endsWith(";")) {
                                        qry = StringUtils.substringBefore(qry, ";");
                                    }
                                    this.query = qry.trim();
                                    String tmp = query.toUpperCase();
                                    console.println(query);
                                    executeCommand();
                                    //System.out.println("Query="+query); 
                                    if (tmp.startsWith("SELECT") || tmp.startsWith("INSERT") || tmp.startsWith("UPDATE") || tmp.startsWith("DELETE")) {
                                        lastSQLQuery = this.query;
                                    }
                                    cmdExecuted = true;
                                }//for
                            }//queries!=null
                        } catch (IOException ex) {
                            console.error(new BroadSQLException(ex));
                        }
                        bs = new StringBuilder();
                    } else {
                        // Execute command from GUI
                        // ========================
                        try {
                            if (!line.startsWith("--")) {
                                bs.append(StringUtils.substringBeforeLast(line, ";"));
                                bs.append(" ");
                            }
                            this.query = bs.toString();
                            String tmp = query.toUpperCase();
                            if (this.query != null) {
                                this.query = this.query.trim();
                            }
                            executeCommand();
                            if (tmp.startsWith("SELECT") || tmp.startsWith("INSERT") || tmp.startsWith("UPDATE") || tmp.startsWith("DELETE")) {
                                lastSQLQuery = this.query;
                            } else if (tmp.startsWith("CONNECT")) {
                                // Execute the user's script
                                executeUserScripts();
                            }
                        } catch (BroadSQLException exc) {
                            cmdLineConsole.println(exc.getText());
                            cmdLineConsole.println("");
                        }
                        cmdExecuted = true;
                        bs = new StringBuilder();
                    }
                } else {
                    if (!line.startsWith("--")) {
                        bs.append(line);
                        bs.append(" ");
                    }
                }

                if (cmdExecuted && query != null && !query.toUpperCase().startsWith("EXT") && !query.toUpperCase().startsWith("EXP")) {
                    this.extractMode = false;
                    this.extractFileName = null;
                    //this.separator = ConsoleSettings.defaultSeparator;
                }
            }//while

            // CLosing the connection
            currentDatabase.close();
            currentDatabase = null;
        }//if connected
    }//runRevised
}//ClassConsoleManager

