/**
* Licensed Materials - Property of IBM
* 5748-XX8
* (C) Copyright IBM Corp. 2013, 2014 All Rights Reserved
* US Government Users Restricted Rights - Use, duplication or
* disclosure restricted by GSA ADP Schedule Contract with
* IBM Corp.
**/

package com.ibm.rational.air.plugin.android;

import com.ibm.rational.air.plugin.android.Util;
import com.urbancode.air.CommandHelper;

/**
* A utility class for helping to run the Android Debug Bridge command.
**/
public class ADBCommandHelper {
    // CommandHelper used to run the ADB command.
    def ch
    // Contains the path to ADB.
    def cmd
    def serialNumber
    def target

    public ADBCommandHelper(String pathToSDK, boolean isWindows) {
        this(pathToSDK, isWindows, null, null);
    }
    
    public ADBCommandHelper(String pathToSDK, boolean isWindows, def devTarget, 
        def devSerialNumber) {
        // Find the Android SDK directory.
        def sdkDir;
        
        try {
            sdkDir = new File(pathToSDK);
        } catch (Exception e) {
            System.out.println("An error occurred during an attempt to access the Android SDK " +
                "directory: " + e.getMessage());
            System.exit(1);
        }

        if(!sdkDir.absolute) {
            def workDir = new File('.').getCanonicalFile();
            sdkDir = new File(workDir, pathToSDK);
        }
        
        if(!sdkDir.directory) {
            System.out.println("Error: The path to the android SDK " +
                    "is incorrect: " + sdkDir.canonicalPath);
            System.exit(1);
        }
        
        // Using the Android SDK directory, locate ADB.
        def adbDir;
        
        try {
            adbDir = new File(sdkDir, File.separator + "platform-tools");
        } catch (Exception e) {
            System.out.println("An error occurred during an attempt to access the Android SDK " +
                "platform-tools directory for the Android Debug Bridge tool: " + e.getMessage());
            System.exit(1);
        }

        // Older ADB SDKs use the tools directory        
        if(!adbDir.directory) {
            try {
                adbDir = new File(sdkDir, File.separator + "tools");
            } catch (Exception e) {
                System.out.println("An error occurred during an attempt to access the Android SDK " +
                    "tools directory for the Android Debug Bridge tool: " +
                    e.getMessage());
                System.exit(1);
            }
        }
     
        def adb = (isWindows?"/adb.exe":"/adb");
        
        try {
            def pathToADBFile = new File(adbDir, adb);
            if(!pathToADBFile.file) {
                System.out.println("Error: The path to the Android Debug Bridge " +
                    "executable file is incorrect: " + pathToADBFile.canonicalPath);
                System.exit(1);
            }
            
            cmd = pathToADBFile.canonicalPath;
        } catch (Exception e) {
            System.out.println("An error occurred during an attempt to access the Android Debug " +
                "Bridge tool: " + e.getMessage());
            System.exit(1);
        }
        
        ch = new CommandHelper(new File('.'));
        
        target = devTarget;
        serialNumber = devSerialNumber;
    }
    
    public String getADBPath() {
        return cmd;
    }
    
    /**
    * Outputs the Android Debug Bridge version.
    **/
    public void printVersion() {
        runADBCmd(null, "version", null, null);
    }
    
    /**
    * Runs the Android Debug Bridge command with the provided optional arguments.
    * arguments: A space-separated list or property file of arguments.
    * timeout: A period after which the Android command is stopped in
    *    milliseconds.
    **/
    public void adbCmd(def arguments, def timeout) {
        def args = [];
        if(arguments) {
            args = Util.handleArgs(arguments, args);
        }
        def result = runADBCmd(null, null, args, timeout);
        if(result != 0) {
            println "Error: An error occurred while the Android Debug Bridge command was running.";
        }
        System.exit(result);
    }
    
    /**
    * Installs the APK on the target.
    * See http://developer.android.com/tools/help/adb.html#move.
    * appPath: The path to the APK application file.
    * arguments: A space-separated list or property file of arguments.
    **/
    public void installApp(boolean reinstall, def appPath, def arguments) {
        def args = [];
        if(reinstall) {
            args << "-r";
        }
        
        if(arguments) {
            args = Util.handleArgs(arguments, args);
        }
        
        args << appPath;
        
        def result = runADBCmd(null, "install", args, null);
        if(result != 0) {
            println "Error: An error occurred during the installation of the application.";
        }
        System.exit(result);
    }
    
    /**
    * Uninstalls the application from the target.
    * See "uninstall" in http://developer.android.com/tools/help/adb.html#pm.
    * keepData: Specifies whether to keep the data.
    * appPath: The package name of the application to remove.
    **/
    public void removeApp(boolean keepData, def pkgName) {
        def result
        // Keeping the data requires the shell and the same APK to fully remove.
        if(keepData) {
            def args = ["pm", "uninstall", "-k", pkgName];
            result = runADBCmdWithShell(null, args, null);
        } else {
            def args = [pkgName];
            result = runADBCmd(null, "uninstall", pkgName, null);
        }
        if(result != 0) {
            println "Error: An error occurred during the removal of the application.";
        }
        System.exit(result);
    }
    
    /**
    * Pushes the files in a space-separated list to the target.
    * See "push" in http://developer.android.com/tools/help/adb.html.
    * filePath: The location on the target to push the files to.
    * files: A space-separated list of files to push.
    **/
    public void pushFilesToTarget(def filePath, def files) {
        def fileList = files.split(" ");
        fileList.each { it ->
            def result = runADBCmd(null, "push", [it, filePath], null);
            if(result != 0) {
                println "Error: An error occurred while the following file was being pushed: " + it;
                System.exit(result);
            }
        }
    }
    
    /**
    * Removes the files in a space-separated list from the target.
    * See "delete" in 
    * http://developer.android.com/tools/help/adb.html#othershellcommands.
    * filePath: The location on the target to remove the files from.
    * files: A space-separated list of files to delete.
    **/
    public void removeFilesFromTarget(def filePath, def files) {
        def fileList = files.split(" ");
        fileList.each { it ->
            def result = runADBCmdWithShell(null, ["rm", filePath + "/" + it], null);
            if(result != 0) {
                println "Error: An error occurred during the removal of this file: " + it;
                System.exit(result);
            }
        }
    }
    
    /**
    * Determines whether the emulator can be started based on the maximum number
    * of concurrent emulators that can be run simultaneously.
    * maxEmulators: The maximum number of concurrent emulators that are allowed.
    **/
    public boolean canStartEmulator(int maxEmulators) {
        def args = [cmd, "devices"];
        def devCH = new CommandHelper(new File('.'));
        
        boolean canStart = true;
        
        // runADBCmd is not used to run the command since the output needs to be read.
        // Find the number of running emulators.
        devCH.runCommand(null, args) {
            proc ->
            def out = new PrintStream(System.out, true)
            def builder = new StringBuilder()
            try {
                // forward stderr to autoflushing output stream, store stdout
                proc.waitForProcessOutput(builder, out)
            }
            finally {
                out.flush();
            }
            def devices = builder.toString();
            List emulatorList = devices.findAll(".*-.*\n");
            if(emulatorList.size() >= maxEmulators) {
                canStart = false;
            } else {
                canStart = true;
            }
        }
        
        return canStart;
    }
    
    /**
    * Checks whether the emulator started based on a maximum retry number for each
    * 20-second interval.
    * startupRetries: The polling frequency. That is, how many times to check every
    *   20 seconds for the emulator status.
    **/
    public void waitForEmulator(int startupRetries) {
        def args = [cmd, "-s", serialNumber, "logcat"];
        
        // runADBCmd is not used to run the command since the output needs to be read.
        def ch = new CommandHelper(new File('.'));
        boolean foundEmulator = false
        int MAX_TRIES = startupRetries;
        int SLEEP = 20000
        
        def builder = new StringBuilder()
        def errOut = new StringBuilder()
    
        // The process is interrupted when the emulator is found.
        // So we ignore the exit value since it will return 1.
        ch.ignoreExitValue(true)
        println "Waiting for the emulator to start."
        try {
            ch.runCommand(null, args) {
                proc ->
                // store stdout and stderr for processing
                proc.consumeProcessOutput(builder, errOut)
    
                for(int i = 0; !foundEmulator && i < MAX_TRIES; i++) {
                    def devices = builder.toString();
                    // New emulators will output BOOT_COMPLETED and the time of day,
                    // while snapshots do not output BOOT_COMPLETED
                    if(devices.contains("Action = android.intent.action.BOOT_COMPLETED") ||
                        devices.contains("Setting time of day to")) {
                        foundEmulator = true
                    } else {
                            Thread.sleep(SLEEP)
                    }
                }
                // End the process and report the status.
                proc.destroy()
            };
        } catch (Exception e) {
            println "An error occurred during an attempt to find a started emulator: ${e.message}"
            e.printStackTrace()
            System.exit(-1)
        }
        
        if(!foundEmulator) {
            println "Error: The emulator was not found. Please check the system."
            System.exit(-1)
        }
        
        println "The emulator was found."
    }
    
    /**
    * Stops the emulator and optionally saves a snapshot of the current
    * state before the shutdown. This function is not in EmulatorHelper because an 
    * Android Debug Bridge shell command is used to check the status.
    * See "stop" in 
    * http://developer.android.com/tools/help/adb.html#othershellcommands.
    * saveSnapshot: Specifies whether to save a snapshot before the emulator is shut down.
    * snapshotName: The name of the snapshot to save.
    **/
    public void stopEmulator(boolean saveSnapshot, def snapshotName) {
        def args = [cmd, "devices"];
        def devCH = new CommandHelper(new File('.'));
        devCH.ignoreExitValue(true);
        // Find the port of the only emulator if a serial number is not 
        // provided.
        def result = devCH.runCommand(null, args) {
            proc ->
            def out = new PrintStream(System.out, true)
            def builder = new StringBuilder()
            try {
                // forward stderr to autoflushing output stream, store stdout
                proc.waitForProcessOutput(builder, out)
            }
            finally {
                out.flush();
            }
            // Find the emulator lines with a dash
            // Emulators that are started from a snapshot show as offline
            // As a result, show all emulators (otherwise sort by lines
            // ending with device).
            def devices = builder.toString();
            List emulatorList = devices.findAll(".*-.*\n");
            if(emulatorList.size() == 0) {
                println "Error: No running emulators found."
                System.exit(-1);
            }
            if(target != "serial") {
                if(emulatorList.size() > 1) {
                    println "Error: A serial number must be specified when " +
                    	"more than one emulator is running."
                    System.exit(-1);
                }
                serialNumber = emulatorList[0].split("\\s")[0];
            }
        }
        if(result != 0) {
            println "Error: An error occurred while the stopped status of the emulator was being checked.";
            System.exit(result);
        }
        
        def serialNumberArray = serialNumber.split("-");
        if(serialNumberArray.length != 2) {
            println "Error: The specified emulator could not be stopped."
            System.exit(-1);
        }
        
        int port = Integer.parseInt(serialNumberArray[1]);
        println "The serial number of the emulator to turn off is " + serialNumber +
            " on this port: " + port;
        
        if(saveSnapshot) {
            if(!snapshotName) {
                println "Error: A snapshot name must be specified when " +
                    "a snapshot is saved.";
                System.exit(-1);
            }
            
            saveToSnapshot(port, snapshotName);
            Thread.sleep(1000);
        }
        requestStopEmulator(port);
        
        // Include a small break between requests.
        Thread.sleep(1000);
      
        // Check the emulator stopped. Otherwise, fail the step.
        result = devCH.runCommand(null, args) {
            proc ->
            def out = new PrintStream(System.out, true)
            def builder = new StringBuilder()
            try {
                // forward stderr to autoflushing output stream, store stdout
                proc.waitForProcessOutput(builder, out)
            }
            finally {
                out.flush();
            }
            // Find the emulator lines with a dash
            // Emulators that are started from a snapshot show as offline
            // As a result, show all emulators (otherwise sort by lines
            // ending with device).
            def devices = builder.toString();
            // Look for the emulator
            List emulatorList = devices.findAll(".*-.*\n");
            for(def i = 0; i < emulatorList.size(); i++) {
                if(emulatorList[i].contains(serialNumber)) {
                    println "Error: The emulator could not be stopped."
                    System.exit(-1);
                }
            }
            
            println "The emulator has stopped."
            System.exit(0);
        }
        if(result != 0) {
            println "Error: An error occurred while the stopped status of the emulator was being checked.";
        }
        System.exit(result);
    }
    
    /**
    * Connects to the emulator and saves a snapshot. 
    * See the help after using Telnet to connect to the 
    * emulator console port.
    * port: The port the emulator is running on.
    * snapshotName: The name of the snapshot to save.
    **/
    private void saveToSnapshot(int port, def snapshotName) {
        println "Saving the snapshot: " + snapshotName;
        boolean requestResult = 
            telnetRequest(port, "avd snapshot save " + snapshotName);
        if(!requestResult) {
            println "Error: An error occurred while the snapshot " +
                "with the following serial number was being saved: " + serialNumber + ".";
            System.exit(-1);
        }
        println "Saving the snapshot is complete: " + snapshotName;
    }
    
    /**
    * Connects to the emulator, stops the running processes, and 
    * then stops the emulator.
    * See "stop" in 
    * http://developer.android.com/tools/help/adb.html#othershellcommands.
    * port: The port that the emulator is running on.
    **/
    private void requestStopEmulator(int port) {
        println "Requesting the emulator stop on port: " + port;
        boolean requestResult = telnetRequest(port, "kill");
        if(!requestResult) {
            println "Error: The request to stop the emulator with the " +
                "following serial number was not completed: " + serialNumber +
                ".";
            System.exit(-1);
        }
        println "The request to stop the emulator on the following port is complete: " + port;
    }
    
    /**
    * Sends a Telnet request to the emulator with the specific command
    * to be run on the emulator. Returns false if an error occurred during
    * the attempt to stop the emulator, true otherwise.
    * port: The port the emulator is running on.
    * command: The command to run on the emulator.
    **/
    private boolean telnetRequest(int port, def command) {    
        Socket socket = null;
        PrintWriter outWriter = null;
        BufferedReader input = null;
        boolean attemptSuccessful = true;
        try {
            socket = new Socket("localhost", port);
            outWriter = new PrintWriter(socket.getOutputStream(), true);
            input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            outWriter.println(command);
            // For saving and shutdown of the emulator, the input can be discarded.
            // Including the reading of the input helps ensure the emulator stops.
            // The first line is the console command
            input.readLine();
            if(command.contains("snapshot")) {
                // Skip the first OK
                input.readLine();
                // Check if the result of the snapshot save is an OK
                if(!"OK".equals(input.readLine())) {
                    attemptSuccessful = false;
                }
            }
            outWriter.flush();
        } catch (UnknownHostException e) {
            attemptSuccessful = false;
            System.out.println("The emulator on the local host was not connected to." +
                "This error message was generated: " + e.getMessage());
            e.printStackTrace();
        } catch (IOException e) {
            attemptSuccessful = false;
            System.out.println("An error occurred in communicating with the emulator: " + 
                e.getMessage());
            e.printStackTrace();
        } finally {
            if(input) {
                try {
                    input.close();
                } catch (IOException e) {
					System.out.println("An error occurred in communicating with " +
						"the emulator while the input stream was being closed: " +
						e.getMessage());
                    e.printStackTrace();
                }
            }
            if(outWriter) {
                outWriter.close();
            }
            try {
                if(socket) {
                    socket.close();
                }
            } catch (IOException e) {
                System.out.println("An error occurred in communicating with the " +
                    "emulator while the socket was being closed: " + e.getMessage());
                e.printStackTrace();
            }
        }
        return attemptSuccessful;
    }
    
    /**
    * Runs a UI test for the specified package on the target.
    * See these pages: http://developer.android.com/tools/testing/testing_ui.html#running,
    *      http://developer.android.com/tools/help/uiautomator/index.html
    * jars: The JAR files that contain the test cases.
    * classes: A list of test classes or methods to run.
    * arguments: A space-separated list or property file of arguments.
    * timeout: A period after which the Android command will be stopped in
    *    milliseconds.
    * Returns the exit code of the command.
    **/
    public int uiTest(def jars, def classes, def arguments, def timeout) {
        def args = ["uiautomator", "runtest"];
        
        // Add the jars to the existing args.
        def jarList = jars.split(" ");
        jarList.each { it ->
            args << it
        }
        
        if(classes) {
            args << "-c";
            args = Util.handleArgs(classes, args);
        }
        
        if(arguments) {
            args = Util.handleArgs(arguments, args);
        }
        
        return runADBCmdWithShell(null, args, timeout);
    }
    
    /**
    * Runs a unit test for the specified package on the target.
    * See http://developer.android.com/tools/testing/testing_otheride.html#AMSyntax.
    * pkgName: The package name of the application to test.
    * runner: The runner to use when the application is tested.
    * rawOutput: Specifies whether to output raw data.
    * arguments: A space-separated list or property file of arguments.
    * timeout: A period after which the Android command is stopped in
    *    milliseconds.
    **/
    public void unitTest(def pkgName, def runner, boolean rawOutput, def arguments,
        def timeout) {
        def args = ["am", "instrument", "-w"];
        
        if(rawOutput) {
            args << "-r";
        }
        
        if(arguments) {
            args = Util.handleArgs(arguments, args);
        }
        
        args << pkgName + "/" + runner;
        
        System.exit(runADBCmdWithShell(null, args, timeout));
    }
    
    /**
    * Runs the monkey program.
    * See http://developer.android.com/tools/help/monkey.html.
    * eventCount: The number of events to send.
    * arguments: A space-separated list or property file of arguments.
    * timeout: A period after which the Android command is stopped in
    *    milliseconds.
    **/    
    public void monkeyCmd(def eventCount, def arguments, def timeout) {
        def args = ["monkey"];
        if(arguments) {
            args = Util.handleArgs(arguments, args);
        }
        
        args << eventCount;
        
        System.exit(runADBCmdWithShell(null, args, timeout)); 
    }
    
    /**
    * Runs the Android Debug Bridge command with the shell argument.
    * See http://developer.android.com/tools/help/adb.html#shellcommands.
    * message: An optional message to output when the command is run.
    * arguments: The arguments to run the command with.
    * timeout: A period after which the Android command is stopped in
    *    milliseconds.
    * Returns the exit code of the command.
    **/
    private int runADBCmdWithShell(def message, def arguments, def timeout) {
        return runADBCmd(message, "shell", arguments, timeout);
    }
    
    /**
    * Runs the Android Debug Bridge command with the provided options.
    * See http://developer.android.com/tools/help/adb.html.
    * message: An optional message to output when the command is run.
    * option: An optional option to run the command with, for example version, shell.
    * arguments: The arguments to run the command with.
    * timeout: A period after which the Android command is stopped in
    *    milliseconds.
    * Returns the exit code of the command.
    **/
    private int runADBCmd(def message, def option, def arguments, def timeout) {
        def cmdArgs = [cmd];
        if(target == "emu") {
            cmdArgs << "-e";
        } else if(target == "dev") {
            cmdArgs << "-d";
        } else if(target == "serial") {
            if(serialNumber) {
                cmdArgs << "-s" << serialNumber;
            } else {
                System.out.println("Error: A serial number must be provided when " +
                    "using the serial number target.");
                return 1;
            }
        }
        
        if(option) {
            cmdArgs << option;
        }
        
        if(arguments) {
            cmdArgs = cmdArgs.plus(cmdArgs.size(), arguments);
        }
        ch.ignoreExitValue = true;
        def result = ch.runCommand(message, cmdArgs) {
            proc ->
            def out = new PrintStream(System.out, true)
            def err = new PrintStream(System.err, true)
            try {
                if(timeout) {
                    // forward stdout and stderr to autoflushing output stream
                    proc.consumeProcessOutput(out, err)
                    println "A timeout of " + timeout + " milliseconds is enabled.";
                    proc.waitForOrKill(Long.parseLong(timeout));
                } else {
                    proc.waitForProcessOutput(out, err);
                }
            }
            finally {
                out.flush();
                err.flush();
            }
        }
        if(result != 0) {
            println "Error: Running the ADB command failed with error code: " + result;
            if(timeout) {
                println "The timeout may have exceeded."
            }
        }
        return result;
    }
}