/*
 * Copyright (C) 2007 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;

import com.android.ddmlib.AndroidDebugBridge;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AndroidConstants;
import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration.TargetMode;
import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
import com.android.sdklib.xml.ManifestData;
import com.android.sdklib.xml.ManifestData.Activity;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.model.LaunchConfigurationDelegate;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;

/**
 * Implementation of an eclipse LauncConfigurationDelegate to launch android
 * application in debug.
 */
public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
    final static int INVALID_DEBUG_PORT = -1;

    public final static String ANDROID_LAUNCH_TYPE_ID =
        "com.android.ide.eclipse.adt.debug.LaunchConfigType"; //$NON-NLS-1$

    /** Target mode parameters: true is automatic, false is manual */
    public static final String ATTR_TARGET_MODE = AdtPlugin.PLUGIN_ID + ".target"; //$NON-NLS-1$
    public static final TargetMode DEFAULT_TARGET_MODE = TargetMode.AUTO;

    /**
     * Launch action:
     * <ul>
     * <li>0: launch default activity</li>
     * <li>1: launch specified activity. See {@link #ATTR_ACTIVITY}</li>
     * <li>2: Do Nothing</li>
     * </ul>
     */
    public final static String ATTR_LAUNCH_ACTION = AdtPlugin.PLUGIN_ID + ".action"; //$NON-NLS-1$

    /** Default launch action. This launches the activity that is setup to be found in the HOME
     * screen.
     */
    public final static int ACTION_DEFAULT = 0;
    /** Launch action starting a specific activity. */
    public final static int ACTION_ACTIVITY = 1;
    /** Launch action that does nothing. */
    public final static int ACTION_DO_NOTHING = 2;
    /** Default launch action value. */
    public final static int DEFAULT_LAUNCH_ACTION = ACTION_DEFAULT;

    /**
     * Activity to be launched if {@link #ATTR_LAUNCH_ACTION} is 1
     */
    public static final String ATTR_ACTIVITY = AdtPlugin.PLUGIN_ID + ".activity"; //$NON-NLS-1$

    public static final String ATTR_AVD_NAME = AdtPlugin.PLUGIN_ID + ".avd"; //$NON-NLS-1$

    public static final String ATTR_SPEED = AdtPlugin.PLUGIN_ID + ".speed"; //$NON-NLS-1$

    /**
     * Index of the default network speed setting for the emulator.<br>
     * Get the emulator option with <code>EmulatorConfigTab.getSpeed(index)</code>
     */
    public static final int DEFAULT_SPEED = 0;

    public static final String ATTR_DELAY = AdtPlugin.PLUGIN_ID + ".delay"; //$NON-NLS-1$

    /**
     * Index of the default network latency setting for the emulator.<br>
     * Get the emulator option with <code>EmulatorConfigTab.getDelay(index)</code>
     */
    public static final int DEFAULT_DELAY = 0;

    public static final String ATTR_COMMANDLINE = AdtPlugin.PLUGIN_ID + ".commandline"; //$NON-NLS-1$

    public static final String ATTR_WIPE_DATA = AdtPlugin.PLUGIN_ID + ".wipedata"; //$NON-NLS-1$
    public static final boolean DEFAULT_WIPE_DATA = false;

    public static final String ATTR_NO_BOOT_ANIM = AdtPlugin.PLUGIN_ID + ".nobootanim"; //$NON-NLS-1$
    public static final boolean DEFAULT_NO_BOOT_ANIM = false;

    public static final String ATTR_DEBUG_PORT =
        AdtPlugin.PLUGIN_ID + ".debugPort"; //$NON-NLS-1$

    public void launch(ILaunchConfiguration configuration, String mode,
            ILaunch launch, IProgressMonitor monitor) throws CoreException {
        // We need to check if it's a standard launch or if it's a launch
        // to debug an application already running.
        int debugPort = AndroidLaunchController.getPortForConfig(configuration);

        // get the project
        IProject project = getProject(configuration);

        // first we make sure the launch is of the proper type
        AndroidLaunch androidLaunch = null;
        if (launch instanceof AndroidLaunch) {
            androidLaunch = (AndroidLaunch)launch;
        } else {
            // wrong type, not sure how we got there, but we don't do
            // anything else
            AdtPlugin.printErrorToConsole(project, "Wrong Launch Type!");
            return;
        }

        // if we have a valid debug port, this means we're debugging an app
        // that's already launched.
        if (debugPort != INVALID_DEBUG_PORT) {
            AndroidLaunchController.launchRemoteDebugger(debugPort, androidLaunch, monitor);
            return;
        }

        if (project == null) {
            AdtPlugin.printErrorToConsole("Couldn't get project object!");
            androidLaunch.stopLaunch();
            return;
        }

        // make sure the project is built. This is a synchronous call which returns when the
        // build is done.
        project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor);

        // check if the project has errors, and abort in this case.
        if (ProjectHelper.hasError(project, true)) {
            AdtPlugin.displayError("Android Launch",
                    "Your project contains error(s), please fix them before running your application.");
            return;
        }

        AdtPlugin.printToConsole(project, "------------------------------"); //$NON-NLS-1$
        AdtPlugin.printToConsole(project, "Android Launch!");

        // check if the project is using the proper sdk.
        // if that throws an exception, we simply let it propagate to the caller.
        if (checkAndroidProject(project) == false) {
            AdtPlugin.printErrorToConsole(project, "Project is not an Android Project. Aborting!");
            androidLaunch.stopLaunch();
            return;
        }

        // Check adb status and abort if needed.
        AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
        if (bridge == null || bridge.isConnected() == false) {
            try {
                int connections = -1;
                int restarts = -1;
                if (bridge != null) {
                    connections = bridge.getConnectionAttemptCount();
                    restarts = bridge.getRestartAttemptCount();
                }

                // if we get -1, the device monitor is not even setup (anymore?).
                // We need to ask the user to restart eclipse.
                // This shouldn't happen, but it's better to let the user know in case it does.
                if (connections == -1 || restarts == -1) {
                    AdtPlugin.printErrorToConsole(project,
                            "The connection to adb is down, and a severe error has occured.",
                            "You must restart adb and Eclipse.",
                            String.format(
                                    "Please ensure that adb is correctly located at '%1$s' and can be executed.",
                                    AdtPlugin.getOsAbsoluteAdb()));
                    return;
                }

                if (restarts == 0) {
                    AdtPlugin.printErrorToConsole(project,
                            "Connection with adb was interrupted.",
                            String.format("%1$s attempts have been made to reconnect.", connections),
                            "You may want to manually restart adb from the Devices view.");
                } else {
                    AdtPlugin.printErrorToConsole(project,
                            "Connection with adb was interrupted, and attempts to reconnect have failed.",
                            String.format("%1$s attempts have been made to restart adb.", restarts),
                            "You may want to manually restart adb from the Devices view.");

                }
                return;
            } finally {
                androidLaunch.stopLaunch();
            }
        }

        // since adb is working, we let the user know
        // TODO have a verbose mode for launch with more info (or some of the less useful info we now have).
        AdtPlugin.printToConsole(project, "adb is running normally.");

        // make a config class
        AndroidLaunchConfiguration config = new AndroidLaunchConfiguration();

        // fill it with the config coming from the ILaunchConfiguration object
        config.set(configuration);

        // get the launch controller singleton
        AndroidLaunchController controller = AndroidLaunchController.getInstance();

        // get the application package
        IFile applicationPackage = ProjectHelper.getApplicationPackage(project);
        if (applicationPackage == null) {
            androidLaunch.stopLaunch();
            return;
        }

        // we need some information from the manifest
        ManifestData manifestData = AndroidManifestHelper.parseForData(project);

        if (manifestData == null) {
            AdtPlugin.printErrorToConsole(project, "Failed to parse AndroidManifest: aborting!");
            androidLaunch.stopLaunch();
            return;
        }

        doLaunch(configuration, mode, monitor, project, androidLaunch, config, controller,
                applicationPackage, manifestData);
    }

    protected void doLaunch(ILaunchConfiguration configuration, String mode,
            IProgressMonitor monitor, IProject project, AndroidLaunch androidLaunch,
            AndroidLaunchConfiguration config, AndroidLaunchController controller,
            IFile applicationPackage, ManifestData manifestData) {

       String activityName = null;

        if (config.mLaunchAction == ACTION_ACTIVITY) {
            // Get the activity name defined in the config
            activityName = getActivityName(configuration);

            // Get the full activity list and make sure the one we got matches.
            Activity[] activities = manifestData.getActivities();

            // first we check that there are, in fact, activities.
            if (activities.length == 0) {
                // if the activities list is null, then the manifest is empty
                // and we can't launch the app. We'll revert to a sync-only launch
                AdtPlugin.printErrorToConsole(project,
                        "The Manifest defines no activity!",
                        "The launch will only sync the application package on the device!");
                config.mLaunchAction = ACTION_DO_NOTHING;
            } else if (activityName == null) {
                // if the activity we got is null, we look for the default one.
                AdtPlugin.printErrorToConsole(project,
                        "No activity specified! Getting the launcher activity.");
                Activity launcherActivity = manifestData.getLauncherActivity();
                if (launcherActivity != null) {
                    activityName = launcherActivity.getName();
                }

                // if there's no default activity. We revert to a sync-only launch.
                if (activityName == null) {
                    revertToNoActionLaunch(project, config);
                }
            } else {

                // check the one we got from the config matches any from the list
                boolean match = false;
                for (Activity a : activities) {
                    if (a != null && a.getName().equals(activityName)) {
                        match = true;
                        break;
                    }
                }

                // if we didn't find a match, we revert to the default activity if any.
                if (match == false) {
                    AdtPlugin.printErrorToConsole(project,
                            "The specified activity does not exist! Getting the launcher activity.");
                    Activity launcherActivity = manifestData.getLauncherActivity();
                    if (launcherActivity != null) {
                        activityName = launcherActivity.getName();
                    } else {
                        // if there's no default activity. We revert to a sync-only launch.
                        revertToNoActionLaunch(project, config);
                    }
                }
            }
        } else if (config.mLaunchAction == ACTION_DEFAULT) {
            Activity launcherActivity = manifestData.getLauncherActivity();
            if (launcherActivity != null) {
                activityName = launcherActivity.getName();
            }

            // if there's no default activity. We revert to a sync-only launch.
            if (activityName == null) {
                revertToNoActionLaunch(project, config);
            }
        }

        IAndroidLaunchAction launchAction = null;
        if (config.mLaunchAction == ACTION_DO_NOTHING || activityName == null) {
            launchAction = new EmptyLaunchAction();
        } else {
            launchAction = new ActivityLaunchAction(activityName, controller);
        }

        // everything seems fine, we ask the launch controller to handle
        // the rest
        controller.launch(project, mode, applicationPackage,manifestData.getPackage(),
                manifestData.getPackage(), manifestData.getDebuggable(),
                manifestData.getMinSdkVersionString(), launchAction, config, androidLaunch,
                monitor);
    }

    @Override
    public boolean buildForLaunch(ILaunchConfiguration configuration,
            String mode, IProgressMonitor monitor) throws CoreException {
        // if this returns true, this forces a full workspace rebuild which is not
        // what we want.
        // Instead in the #launch method, we'll rebuild only the launching project.
        return false;
    }

    /**
     * {@inheritDoc}
     * @throws CoreException
     */
    @Override
    public ILaunch getLaunch(ILaunchConfiguration configuration, String mode)
            throws CoreException {
        return new AndroidLaunch(configuration, mode, null);
    }

    /**
     * Returns the IProject object matching the name found in the configuration
     * object under the name
     * <code>IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME</code>
     * @param configuration
     * @return The IProject object or null
     */
    private IProject getProject(ILaunchConfiguration configuration){
        // get the project name from the config
        String projectName;
        try {
            projectName = configuration.getAttribute(
                    IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, "");
        } catch (CoreException e) {
            return null;
        }

        // get the current workspace
        IWorkspace workspace = ResourcesPlugin.getWorkspace();

        // and return the project with the name from the config
        return workspace.getRoot().getProject(projectName);
    }

    /**
     * Checks the project is an android project.
     * @param project The project to check
     * @return true if the project is an android SDK.
     * @throws CoreException
     */
    private boolean checkAndroidProject(IProject project) throws CoreException {
        // check if the project is a java and an android project.
        if (project.hasNature(JavaCore.NATURE_ID) == false) {
            String msg = String.format("%1$s is not a Java project!", project.getName());
            AdtPlugin.displayError("Android Launch", msg);
            return false;
        }

        if (project.hasNature(AndroidConstants.NATURE_DEFAULT) == false) {
            String msg = String.format("%1$s is not an Android project!", project.getName());
            AdtPlugin.displayError("Android Launch", msg);
            return false;
        }

        return true;
    }


    /**
     * Returns the name of the activity.
     */
    private String getActivityName(ILaunchConfiguration configuration) {
        String empty = "";
        String activityName;
        try {
            activityName = configuration.getAttribute(ATTR_ACTIVITY, empty);
        } catch (CoreException e) {
            return null;
        }

        return (activityName != empty) ? activityName : null;
    }

    private final void revertToNoActionLaunch(IProject project, AndroidLaunchConfiguration config) {
        AdtPlugin.printErrorToConsole(project,
                "No Launcher activity found!",
                "The launch will only sync the application package on the device!");
        config.mLaunchAction = ACTION_DO_NOTHING;
    }
}
