/*
 * Copyright 2010 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.drools.eclipse.launching;

import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.drools.core.base.mvel.MVELDebugHandler;
import org.drools.eclipse.debug.core.DroolsDebugModel;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.IStatusHandler;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IStreamsProxy;
import org.eclipse.jdi.Bootstrap;
import org.eclipse.jdt.internal.launching.LaunchingMessages;
import org.eclipse.jdt.internal.launching.LaunchingPlugin;
import org.eclipse.jdt.internal.launching.LibraryInfo;
import org.eclipse.jdt.internal.launching.StandardVMDebugger;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jdt.launching.SocketUtil;
import org.eclipse.jdt.launching.VMRunnerConfiguration;
import org.eclipse.osgi.util.NLS;

import com.sun.jdi.VirtualMachine;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.Connector.Argument;
import com.sun.jdi.connect.IllegalConnectorArgumentsException;
import com.sun.jdi.connect.ListeningConnector;

public class DroolsVMDebugger extends StandardVMDebugger {

    class ConnectRunnable implements Runnable {

        private VirtualMachine fVirtualMachine = null;
        private ListeningConnector fConnector = null;
        private Map<String, Argument> fConnectionMap = null;
        private Exception fException = null;

        public ConnectRunnable(ListeningConnector connector, Map<String, Argument> map) {
            fConnector = connector;
            fConnectionMap = map;
        }

        public void run() {
            try {
                fVirtualMachine = fConnector.accept(fConnectionMap);
            } catch (IOException e) {
                fException = e;
            } catch (IllegalConnectorArgumentsException e) {
                fException = e;
            }
        }

        public VirtualMachine getVirtualMachine() {
            return fVirtualMachine;
        }

        public Exception getException() {
            return fException;
        }
    }

    public DroolsVMDebugger(IVMInstall vmInstance) {
        super(vmInstance);
    }

    public void run(VMRunnerConfiguration config, ILaunch launch, IProgressMonitor monitor) throws CoreException {

        if (monitor == null) {
            monitor = new NullProgressMonitor();
        }

        IProgressMonitor subMonitor = new SubProgressMonitor(monitor, 1);
        subMonitor.beginTask(LaunchingMessages.StandardVMDebugger_Launching_VM____1, 4);
        subMonitor.subTask(LaunchingMessages.StandardVMDebugger_Finding_free_socket____2);

        int port= SocketUtil.findFreePort();
        if (port == -1) {
            abort(LaunchingMessages.StandardVMDebugger_Could_not_find_a_free_socket_for_the_debugger_1, null, IJavaLaunchConfigurationConstants.ERR_NO_SOCKET_AVAILABLE);
        }

        subMonitor.worked(1);

        // check for cancellation
        if (monitor.isCanceled()) {
            return;
        }

        subMonitor.subTask(LaunchingMessages.StandardVMDebugger_Constructing_command_line____3);

        String program= constructProgramString(config);

        List<String> arguments= new ArrayList<String>(12);

        arguments.add(program);

        // VM args are the first thing after the java program so that users can specify
        // options like '-client' & '-server' which are required to be the first options
        String[] allVMArgs = combineVmArgs(config, fVMInstance);
        addArguments(allVMArgs, arguments);
        arguments.add("-D"+MVELDebugHandler.DEBUG_LAUNCH_KEY+"=true");
        
        addBootClassPathArguments(arguments, config);

        String[] cp= config.getClassPath();
        if (cp.length > 0) {
            arguments.add("-classpath"); //$NON-NLS-1$
            arguments.add(convertClassPath(cp));
        }
        double version = getJavaVersion();
        if (version < 1.5) {
            arguments.add("-Xdebug"); //$NON-NLS-1$
            arguments.add("-Xnoagent"); //$NON-NLS-1$
        }

        //check if java 1.4 or greater
        if (version < 1.4) {
            arguments.add("-Djava.compiler=NONE"); //$NON-NLS-1$
        }
        if (version < 1.5) {
            arguments.add("-Xrunjdwp:transport=dt_socket,suspend=y,address=localhost:" + port); //$NON-NLS-1$
        } else {
            arguments.add("-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:" + port); //$NON-NLS-1$
        }

        arguments.add(config.getClassToLaunch());
        addArguments(config.getProgramArguments(), arguments);
        
        String[] cmdLine= new String[arguments.size()];
        arguments.toArray(cmdLine);

        String[] envp= config.getEnvironment();

        // check for cancellation
        if (monitor.isCanceled()) {
            return;
        }

        subMonitor.worked(1);
        subMonitor.subTask(LaunchingMessages.StandardVMDebugger_Starting_virtual_machine____4);

        ListeningConnector connector= getConnector();
        if (connector == null) {
            abort(LaunchingMessages.StandardVMDebugger_Couldn__t_find_an_appropriate_debug_connector_2, null, IJavaLaunchConfigurationConstants.ERR_CONNECTOR_NOT_AVAILABLE);
        }
        Map<String, Argument> map= connector.defaultArguments();

        specifyArguments(map, port);
        Process p= null;
        try {
            try {
                // check for cancellation
                if (monitor.isCanceled()) {
                    return;
                }

                connector.startListening(map);

                File workingDir = getWorkingDir(config);
                p = exec(cmdLine, workingDir, envp);
                if (p == null) {
                    return;
                }

                // check for cancellation
                if (monitor.isCanceled()) {
                    p.destroy();
                    return;
                }
                java.util.Date date= new java.util.Date();
                Timestamp ts = new Timestamp(date.getTime());
                String format= LaunchingMessages.StandardVMRunner__0____1___2; 
        		String label = NLS.bind(format, new String[] { cmdLine[0], ts.toString() });
                IProcess process= newProcess(launch, p, label, getDefaultProcessMap());
                process.setAttribute(IProcess.ATTR_CMDLINE, renderCommandLineInternal(cmdLine));
                subMonitor.worked(1);
                subMonitor.subTask(LaunchingMessages.StandardVMDebugger_Establishing_debug_connection____5);
                boolean retry= false;
                do  {
                    try {

                        ConnectRunnable runnable = new ConnectRunnable(connector, map);
                        Thread connectThread = new Thread(runnable, "Listening Connector"); //$NON-NLS-1$
                        connectThread.setDaemon(true);
                        connectThread.start();
                        while (connectThread.isAlive()) {
                            if (monitor.isCanceled()) {
                                connector.stopListening(map);
                                p.destroy();
                                return;
                            }
                            try {
                                p.exitValue();
                                // process has terminated - stop waiting for a connection
                                try {
                                    connector.stopListening(map);
                                } catch (IOException e) {
                                    // expected
                                }
                                checkErrorMessage(process);
                            } catch (IllegalThreadStateException e) {
                                // expected while process is alive
                            }
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                            }
                        }

                        Exception ex = runnable.getException();
                        if (ex instanceof IllegalConnectorArgumentsException)                         {
                            throw (IllegalConnectorArgumentsException)ex;
                        }
                        if (ex instanceof InterruptedIOException) {
                            throw (InterruptedIOException)ex;
                        }
                        if (ex instanceof IOException) {
                            throw (IOException)ex;
                        }

                        VirtualMachine vm= runnable.getVirtualMachine();
                        if (vm != null) {
                            DroolsDebugModel.newDebugTarget(launch, vm, renderDebugTarget(config.getClassToLaunch(), port), process, true, false, config.isResumeOnStartup());
                            subMonitor.worked(1);
                            subMonitor.done();
                        }
                        return;
                    } catch (InterruptedIOException e) {
                        checkErrorMessage(process);

                        // timeout, consult status handler if there is one
                        IStatus status = new Status(IStatus.ERROR, LaunchingPlugin.getUniqueIdentifier(), IJavaLaunchConfigurationConstants.ERR_VM_CONNECT_TIMEOUT, "", e); //$NON-NLS-1$
                        IStatusHandler handler = DebugPlugin.getDefault().getStatusHandler(status);

                        retry= false;
                        if (handler == null) {
                            // if there is no handler, throw the exception
                            throw new CoreException(status);
                        }
                        Object result = handler.handleStatus(status, this);
                        if (result instanceof Boolean) {
                            retry = ((Boolean)result).booleanValue();
                        }
                    }
                } while (retry);
            } finally {
                connector.stopListening(map);
            }
        } catch (IOException e) {
            abort(LaunchingMessages.StandardVMDebugger_Couldn__t_connect_to_VM_4, e, IJavaLaunchConfigurationConstants.ERR_CONNECTION_FAILED);
        } catch (IllegalConnectorArgumentsException e) {
            abort(LaunchingMessages.StandardVMDebugger_Couldn__t_connect_to_VM_5, e, IJavaLaunchConfigurationConstants.ERR_CONNECTION_FAILED);
        }
        if (p != null) {
            p.destroy();
        }
    }

    private String renderCommandLineInternal(String[] commandLine) {
        if (commandLine.length < 1)
            return ""; //$NON-NLS-1$
        StringBuffer buf= new StringBuffer();
        for (int i= 0; i < commandLine.length; i++) {
            buf.append(' ');
            char[] characters= commandLine[i].toCharArray();
            StringBuffer command= new StringBuffer();
            boolean containsSpace= false;
            for (int j = 0; j < characters.length; j++) {
                char character= characters[j];
                if (character == '\"') {
                    command.append('\\');
                } else if (character == ' ') {
                    containsSpace = true;
                }
                command.append(character);
            }
            if (containsSpace) {
                buf.append('\"');
                buf.append(command.toString());
                buf.append('\"');
            } else {
                buf.append(command.toString());
            }
        }
        return buf.toString();
    }

    private double getJavaVersion() {
        LibraryInfo libInfo = LaunchingPlugin.getLibraryInfo(fVMInstance.getInstallLocation().getAbsolutePath());
        if (libInfo == null) {
            return 0D;
        }
        String version = libInfo.getVersion();
        int index = version.indexOf("."); //$NON-NLS-1$
        int nextIndex = version.indexOf(".", index+1); //$NON-NLS-1$
        try {
            if (index > 0 && nextIndex>index) {
                return Double.parseDouble(version.substring(0,nextIndex));
            }
            return Double.parseDouble(version);
        } catch (NumberFormatException e) {
            return 0D;
        }

    }

    protected void checkErrorMessage(IProcess process) throws CoreException {
        IStreamsProxy streamsProxy = process.getStreamsProxy();
        if (streamsProxy != null) {
            String errorMessage= streamsProxy.getErrorStreamMonitor().getContents();
            if (errorMessage.length() == 0) {
                errorMessage= streamsProxy.getOutputStreamMonitor().getContents();
            }
            if (errorMessage.length() != 0) {
                abort(errorMessage, null, IJavaLaunchConfigurationConstants.ERR_VM_LAUNCH_ERROR);
            }
        }
    }

    protected void specifyArguments(Map map, int portNumber) {
        // XXX: Revisit - allows us to put a quote (") around the classpath
        Connector.IntegerArgument port= (Connector.IntegerArgument) map.get("port"); //$NON-NLS-1$
        port.setValue(portNumber);

        Connector.IntegerArgument timeoutArg= (Connector.IntegerArgument) map.get("timeout"); //$NON-NLS-1$
        if (timeoutArg != null) {
            int timeout = JavaRuntime.getPreferences().getInt(JavaRuntime.PREF_CONNECT_TIMEOUT);
            timeoutArg.setValue(timeout);
        }
    }

    protected ListeningConnector getConnector() {
        List<ListeningConnector> connectors= Bootstrap.virtualMachineManager().listeningConnectors();
        for (int i= 0; i < connectors.size(); i++) {
            ListeningConnector c= connectors.get(i);
            if ("com.sun.jdi.SocketListen".equals(c.name())) //$NON-NLS-1$
                return c;
        }
        return null;
    }

}
