/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
 *
 * 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 com.android.ide.eclipse.adt.internal.launch.junit;

import com.android.ddmlib.IDevice;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.launch.DelayedLaunchInfo;
import com.android.ide.eclipse.adt.internal.launch.IAndroidLaunchAction;
import com.android.ide.eclipse.adt.internal.launch.LaunchMessages;
import com.android.ide.eclipse.adt.internal.launch.junit.runtime.AndroidJUnitLaunchInfo;
import com.android.ide.eclipse.adt.internal.launch.junit.runtime.RemoteAdtTestRunner;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IStreamsProxy;
import org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate;
import org.eclipse.jdt.launching.IVMRunner;
import org.eclipse.jdt.launching.VMRunnerConfiguration;
import org.eclipse.swt.widgets.Display;

/**
 * A launch action that executes a instrumentation test run on an Android device.
 */
class AndroidJUnitLaunchAction implements IAndroidLaunchAction {

    private final AndroidJUnitLaunchInfo mLaunchInfo;
    
    /**
     * Creates a AndroidJUnitLaunchAction.
     * 
     * @param launchInfo the {@link AndroidJUnitLaunchInfo} for the JUnit run 
     */
    public AndroidJUnitLaunchAction(AndroidJUnitLaunchInfo launchInfo) {
        mLaunchInfo = launchInfo;
    }
    
    /**
     * Launch a instrumentation test run on given Android device. 
     * Reuses JDT JUnit launch delegate so results can be communicated back to JDT JUnit UI.
     * <p/>
     * Note: Must be executed on non-UI thread.
     * 
     * @see IAndroidLaunchAction#doLaunchAction(DelayedLaunchInfo, IDevice)
     */
    public boolean doLaunchAction(DelayedLaunchInfo info, IDevice device) {
        String msg = String.format(LaunchMessages.AndroidJUnitLaunchAction_LaunchInstr_2s,
                mLaunchInfo.getRunner(), device.getSerialNumber());
        AdtPlugin.printToConsole(info.getProject(), msg);
        
        try {
           mLaunchInfo.setDebugMode(info.isDebugMode());
           mLaunchInfo.setDevice(info.getDevice());
           JUnitLaunchDelegate junitDelegate = new JUnitLaunchDelegate(mLaunchInfo);
           final String mode = info.isDebugMode() ? ILaunchManager.DEBUG_MODE : 
               ILaunchManager.RUN_MODE; 

           junitDelegate.launch(info.getLaunch().getLaunchConfiguration(), mode, info.getLaunch(),
                   info.getMonitor());

           // TODO: need to add AMReceiver-type functionality somewhere
        } catch (CoreException e) {
            AdtPlugin.printErrorToConsole(info.getProject(),
                    LaunchMessages.AndroidJUnitLaunchAction_LaunchFail);
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public String getLaunchDescription() {
        return String.format(LaunchMessages.AndroidJUnitLaunchAction_LaunchDesc_s,
                mLaunchInfo.getRunner());
    }

    /**
     * Extends the JDT JUnit launch delegate to allow for JUnit UI reuse. 
     */
    private static class JUnitLaunchDelegate extends JUnitLaunchConfigurationDelegate {
        
        private AndroidJUnitLaunchInfo mLaunchInfo;

        public JUnitLaunchDelegate(AndroidJUnitLaunchInfo launchInfo) {
            mLaunchInfo = launchInfo;
        }

        /* (non-Javadoc)
         * @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#launch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.debug.core.ILaunch, org.eclipse.core.runtime.IProgressMonitor)
         */
        @Override
        public synchronized void launch(ILaunchConfiguration configuration, String mode,
                ILaunch launch, IProgressMonitor monitor) throws CoreException {
            // TODO: is progress monitor adjustment needed here?
            super.launch(configuration, mode, launch, monitor);
        }

        /**
         * {@inheritDoc}
         * @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#verifyMainTypeName(org.eclipse.debug.core.ILaunchConfiguration)
         */
        @Override
        public String verifyMainTypeName(ILaunchConfiguration configuration) {
            return "com.android.ide.eclipse.adt.junit.internal.runner.RemoteAndroidTestRunner"; //$NON-NLS-1$
        }

        /**
         * Overrides parent to return a VM Runner implementation which launches a thread, rather
         * than a separate VM process
         */
        @Override
        public IVMRunner getVMRunner(ILaunchConfiguration configuration, String mode) {
            return new VMTestRunner(mLaunchInfo);
        }

        /**
         * {@inheritDoc}
         * @see org.eclipse.debug.core.model.LaunchConfigurationDelegate#getLaunch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String)
         */
        @Override
        public ILaunch getLaunch(ILaunchConfiguration configuration, String mode) {
            return mLaunchInfo.getLaunch();
        }     
    }

    /**
     * Provides a VM runner implementation which starts a inline implementation of a launch process
     */
    private static class VMTestRunner implements IVMRunner {
        
        private final AndroidJUnitLaunchInfo mJUnitInfo;
        
        VMTestRunner(AndroidJUnitLaunchInfo info) {
            mJUnitInfo = info;
        }

        /**
         * {@inheritDoc}
         * @throws CoreException
         */
        public void run(final VMRunnerConfiguration config, ILaunch launch,
                IProgressMonitor monitor) throws CoreException {
            
            TestRunnerProcess runnerProcess = 
                new TestRunnerProcess(config, mJUnitInfo);
            launch.addProcess(runnerProcess);
            runnerProcess.run();
        }
    }

    /**
     * Launch process that executes the tests.
     */
    private static class TestRunnerProcess implements IProcess  {

        private final VMRunnerConfiguration mRunConfig;
        private final AndroidJUnitLaunchInfo mJUnitInfo;
        private RemoteAdtTestRunner mTestRunner = null;
        private boolean mIsTerminated = false;
        
        TestRunnerProcess(VMRunnerConfiguration runConfig, AndroidJUnitLaunchInfo info) {
            mRunConfig = runConfig;
            mJUnitInfo = info;
        }
        
        /* (non-Javadoc)
         * @see org.eclipse.debug.core.model.IProcess#getAttribute(java.lang.String)
         */
        public String getAttribute(String key) {
            return null;
        }

        /**
         * {@inheritDoc}
         * @see org.eclipse.debug.core.model.IProcess#getExitValue()
         */
        public int getExitValue() {
            return 0;
        }

        /* (non-Javadoc)
         * @see org.eclipse.debug.core.model.IProcess#getLabel()
         */
        public String getLabel() {
            return mJUnitInfo.getLaunch().getLaunchMode();
        }

        /* (non-Javadoc)
         * @see org.eclipse.debug.core.model.IProcess#getLaunch()
         */
        public ILaunch getLaunch() {
            return mJUnitInfo.getLaunch();
        }

        /* (non-Javadoc)
         * @see org.eclipse.debug.core.model.IProcess#getStreamsProxy()
         */
        public IStreamsProxy getStreamsProxy() {
            return null;
        }

        /* (non-Javadoc)
         * @see org.eclipse.debug.core.model.IProcess#setAttribute(java.lang.String, 
         * java.lang.String)
         */
        public void setAttribute(String key, String value) {
            // ignore           
        }

        /* (non-Javadoc)
         * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
         */
        @SuppressWarnings("unchecked")
        public Object getAdapter(Class adapter) {
            return null;
        }

        /* (non-Javadoc)
         * @see org.eclipse.debug.core.model.ITerminate#canTerminate()
         */
        public boolean canTerminate() {
            return true;
        }

        /* (non-Javadoc)
         * @see org.eclipse.debug.core.model.ITerminate#isTerminated()
         */
        public boolean isTerminated() {
            return mIsTerminated;
        }

        /**
         * {@inheritDoc}
         * @see org.eclipse.debug.core.model.ITerminate#terminate()
         */
        public void terminate() {
            if (mTestRunner != null) {
                mTestRunner.terminate();
            }    
            mIsTerminated = true;
        } 

        /**
         * Launches a test runner that will communicate results back to JDT JUnit UI.
         * <p/>
         * Must be executed on a non-UI thread.
         */
        public void run() {
            if (Display.getCurrent() != null) {
                AdtPlugin.log(IStatus.ERROR, "Adt test runner executed on UI thread");
                AdtPlugin.printErrorToConsole(mJUnitInfo.getProject(),
                        "Test launch failed due to internal error: Running tests on UI thread");
                terminate();
                return;
            }
            mTestRunner = new RemoteAdtTestRunner();
            mTestRunner.runTests(mRunConfig.getProgramArguments(), mJUnitInfo);
        }
    }
}

