/*
 * 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.
 */

/*
 * References:
 * org.eclipse.jdt.internal.ui.wizards.JavaProjectWizard
 * org.eclipse.jdt.internal.ui.wizards.JavaProjectWizardFirstPage
 */

package com.android.ide.eclipse.adt.internal.wizards.newproject;

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreationPage.IMainInfo;
import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreationPage.MainInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.xml.ManifestData;
import com.android.sdkuilib.internal.widgets.SdkTargetSelector;

import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaConventions;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.osgi.util.TextProcessor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkingSet;

import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.regex.Pattern;

/**
 * NewAndroidProjectCreationPage is a project creation page that provides the
 * following fields:
 * <ul>
 * <li> Project name
 * <li> SDK Target
 * <li> Application name
 * <li> Package name
 * <li> Activity name
 * </ul>
 * Note: this class is public so that it can be accessed from unit tests.
 * It is however an internal class. Its API may change without notice.
 * It should semantically be considered as a private final class.
 * <p/>
 * Do not derive from this class.
 */
public class NewTestProjectCreationPage extends WizardPage {

    // constants
    static final String TEST_PAGE_NAME = "newAndroidTestProjectPage"; //$NON-NLS-1$

    /** Initial value for all name fields (project, activity, application, package). Used
     * whenever a value is requested before controls are created. */
    private static final String INITIAL_NAME = "";  //$NON-NLS-1$
    /** Initial value for the Use Default Location check box. */
    private static final boolean INITIAL_USE_DEFAULT_LOCATION = true;
    /** Initial value for the Create Test Project check box. */
    private static final boolean INITIAL_CREATE_TEST_PROJECT = false;


    /** Pattern for characters accepted in a project name. Since this will be used as a
     * directory name, we're being a bit conservative on purpose. It cannot start with a space. */
    private static final Pattern sProjectNamePattern = Pattern.compile("^[\\w][\\w. -]*$");  //$NON-NLS-1$
    /** Last user-browsed location, static so that it be remembered for the whole session */
    private static String sCustomLocationOsPath = "";  //$NON-NLS-1$

    private final int MSG_NONE = 0;
    private final int MSG_WARNING = 1;
    private final int MSG_ERROR = 2;

    /** Structure with the externally visible information from this Test Project page. */
    private final TestInfo mInfo = new TestInfo();
    /** Structure with the externally visible information from the Main Project page.
     *  This is null if there's no such page, meaning the test project page is standalone. */
    private IMainInfo mMainInfo;

    // widgets
    private Text mProjectNameField;
    private Text mPackageNameField;
    private Text mApplicationNameField;
    private Button mUseDefaultLocation;
    private Label mLocationLabel;
    private Text mLocationPathField;
    private Button mBrowseButton;
    private Text mMinSdkVersionField;
    private SdkTargetSelector mSdkTargetSelector;
    private ITargetChangeListener mSdkTargetChangeListener;
    private Button mCreateTestProjectField;
    private Text mTestedProjectNameField;
    private Button mProjectBrowseButton;
    private ProjectChooserHelper mProjectChooserHelper;
    private Button mTestSelfProjectRadio;
    private Button mTestExistingProjectRadio;

    /** A list of composites that are disabled when the "Create Test Project" toggle is off. */
    private ArrayList<Composite> mToggleComposites = new ArrayList<Composite>();

    private boolean mInternalProjectNameUpdate;
    private boolean mInternalLocationPathUpdate;
    private boolean mInternalPackageNameUpdate;
    private boolean mInternalApplicationNameUpdate;
    private boolean mInternalMinSdkVersionUpdate;
    private boolean mInternalSdkTargetUpdate;
    private IProject mExistingTestedProject;
    private boolean mProjectNameModifiedByUser;
    private boolean mApplicationNameModifiedByUser;
    private boolean mPackageNameModifiedByUser;
    private boolean mMinSdkVersionModifiedByUser;
    private boolean mSdkTargetModifiedByUser;

    private Label mTestTargetPackageLabel;

    private String mLastExistingPackageName;
    private WorkingSetGroup mWorkingSetGroup;


    /**
     * Creates a new project creation wizard page.
     */
    public NewTestProjectCreationPage() {
        super(TEST_PAGE_NAME);
        setPageComplete(false);
        setTitle("New Android Test Project");
        setDescription("Creates a new Android Test Project resource.");
        mWorkingSetGroup= new WorkingSetGroup();
        setWorkingSets(new IWorkingSet[0]);
    }

    public void init(IStructuredSelection selection, IWorkbenchPart activePart) {
        setWorkingSets(WorkingSetHelper.getSelectedWorkingSet(selection, activePart));
    }

    // --- Getters used by NewProjectWizard ---

    /**
     * Structure that collects all externally visible information from this page.
     * This is used by the calling wizard to actually do the work or by other pages.
     */
    public class TestInfo {

        /** Returns true if a new Test Project should be created. */
        public boolean getCreateTestProject() {
            return mCreateTestProjectField == null ? true : mCreateTestProjectField.getSelection();
        }

        /**
         * Returns the current project location path as entered by the user, or its
         * anticipated initial value. Note that if the default has been returned the
         * path in a project description used to create a project should not be set.
         *
         * @return the project location path or its anticipated initial value.
         */
        public IPath getLocationPath() {
            return new Path(getProjectLocation());
        }

        /** Returns the value of the project name field with leading and trailing spaces removed. */
        public String getProjectName() {
            return mProjectNameField == null ? INITIAL_NAME : mProjectNameField.getText().trim();
        }

        /** Returns the value of the package name field with spaces trimmed. */
        public String getPackageName() {
            return mPackageNameField == null ? INITIAL_NAME : mPackageNameField.getText().trim();
        }

        /** Returns the value of the test target package name field with spaces trimmed. */
        public String getTargetPackageName() {
            return mTestTargetPackageLabel == null ? INITIAL_NAME
                                                   : mTestTargetPackageLabel.getText().trim();
        }

        /** Returns the value of the min sdk version field with spaces trimmed. */
        public String getMinSdkVersion() {
            return mMinSdkVersionField == null ? "" : mMinSdkVersionField.getText().trim();  //$NON-NLS-1$
        }

        /** Returns the value of the application name field with spaces trimmed. */
        public String getApplicationName() {
            // Return the name of the activity as default application name.
            return mApplicationNameField == null ? "" : mApplicationNameField.getText().trim();  //$NON-NLS-1$
        }

        /** Returns the value of the Use Default Location field. */
        public boolean useDefaultLocation() {
            return mUseDefaultLocation == null ? INITIAL_USE_DEFAULT_LOCATION
                                               : mUseDefaultLocation.getSelection();
        }

        /** Returns the the default "src" constant. */
        public String getSourceFolder() {
            return SdkConstants.FD_SOURCES;
        }

        /** Returns the current sdk target or null if none has been selected yet. */
        public IAndroidTarget getSdkTarget() {
            return mSdkTargetSelector == null ? null : mSdkTargetSelector.getSelected();
        }

        public boolean isTestingSelf() {
            return mMainInfo == null &&
                (mTestSelfProjectRadio == null ? false : mTestSelfProjectRadio.getSelection());
        }

        public boolean isTestingMain() {
            return mMainInfo != null;
        }

        public boolean isTestingExisting() {
            return mMainInfo == null &&
                (mTestExistingProjectRadio == null ? false
                                                   : mTestExistingProjectRadio.getSelection());
        }

        public IProject getExistingTestedProject() {
            return mExistingTestedProject;
        }
    }

    /**
     * Returns a {@link TestInfo} structure that collects all externally visible information
     * from this page. This is used by the calling wizard to actually do the work or by other pages.
     */
    public TestInfo getTestInfo() {
        return mInfo;
    }

    /**
     * Grabs the {@link MainInfo} structure with visible parameters from the main project page.
     * This may be null.
     */
    public void setMainInfo(IMainInfo mainInfo) {
        mMainInfo = mainInfo;
    }

    // --- UI creation ---

    /**
     * Creates the top level control for this dialog page under the given parent
     * composite.
     *
     * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite)
     */
    public void createControl(Composite parent) {
        final ScrolledComposite scrolledComposite = new ScrolledComposite(parent, SWT.V_SCROLL);
        scrolledComposite.setFont(parent.getFont());
        scrolledComposite.setExpandHorizontal(true);
        scrolledComposite.setExpandVertical(true);
        initializeDialogUnits(parent);

        final Composite composite = new Composite(scrolledComposite, SWT.NULL);
        composite.setFont(parent.getFont());
        scrolledComposite.setContent(composite);

        composite.setLayout(new GridLayout());
        composite.setLayoutData(new GridData(GridData.FILL_BOTH));

        createToggleTestProject(composite);
        createTestProjectGroup(composite);
        createLocationGroup(composite);
        createTestTargetGroup(composite);
        createTargetGroup(composite);
        createPropertiesGroup(composite);
        createWorkingSetGroup(composite);

        // Update state the first time
        enableLocationWidgets();

        scrolledComposite.addControlListener(new ControlAdapter() {
            @Override
            public void controlResized(ControlEvent e) {
                Rectangle r = scrolledComposite.getClientArea();
                scrolledComposite.setMinSize(composite.computeSize(r.width, SWT.DEFAULT));
            }
        });

        // Show description the first time
        setErrorMessage(null);
        setMessage(null);
        setControl(scrolledComposite);

        // Validate. This will complain about the first empty field.
        validatePageComplete();
    }

    /**
     * Overrides @DialogPage.setVisible(boolean) to put the focus in the project name when
     * the dialog is made visible and to also update the enabled/disabled state of some
     * controls (doing so in createControl doesn't always change their state somehow.)
     */
    @Override
    public void setVisible(boolean visible) {
        super.setVisible(visible);
        if (visible) {
            mProjectNameField.setFocus();
            validatePageComplete();
            onCreateTestProjectToggle();
            onExistingProjectChanged();
        }
    }

    @Override
    public void dispose() {

        if (mSdkTargetChangeListener != null) {
            AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener);
            mSdkTargetChangeListener = null;
        }

        super.dispose();
    }

    /**
     * Creates the "create test project" checkbox but only if there's a main page in the wizard.
     *
     * @param parent the parent composite
     */
    private final void createToggleTestProject(Composite parent) {

        if (mMainInfo != null) {
            mCreateTestProjectField = new Button(parent, SWT.CHECK);
            mCreateTestProjectField.setText("Create a Test Project");
            mCreateTestProjectField.setToolTipText("Select this if you also want to create a Test Project.");
            mCreateTestProjectField.setSelection(INITIAL_CREATE_TEST_PROJECT);
            mCreateTestProjectField.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    onCreateTestProjectToggle();
                }
            });
        }
    }

    /**
     * Creates the group for the project name:
     * [label: "Project Name"] [text field]
     *
     * @param parent the parent composite
     */
    private final void createTestProjectGroup(Composite parent) {
        Composite group = new Composite(parent, SWT.NONE);
        GridLayout layout = new GridLayout();
        layout.numColumns = 2;
        group.setLayout(layout);
        group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        mToggleComposites.add(group);

        // --- test project name ---

        // new project label
        String tooltip = "Name of the Eclipse test project to create. It cannot be empty.";
        Label label = new Label(group, SWT.NONE);
        label.setText("Test Project Name:");
        label.setFont(parent.getFont());
        label.setToolTipText(tooltip);

        // new project name entry field
        mProjectNameField = new Text(group, SWT.BORDER);
        GridData data = new GridData(GridData.FILL_HORIZONTAL);
        mProjectNameField.setToolTipText(tooltip);
        mProjectNameField.setLayoutData(data);
        mProjectNameField.setFont(parent.getFont());
        mProjectNameField.addListener(SWT.Modify, new Listener() {
            public void handleEvent(Event event) {
                if (!mInternalProjectNameUpdate) {
                    mProjectNameModifiedByUser = true;
                }
                updateLocationPathField(null);
            }
        });
    }

    private final void createLocationGroup(Composite parent) {

        // --- project location ---

        Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
        group.setLayout(new GridLayout(3, /* num columns */
                false /* columns of not equal size */));
        group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        group.setFont(parent.getFont());
        group.setText("Content");

        mToggleComposites.add(group);

        mUseDefaultLocation = new Button(group, SWT.CHECK);
        GridData gd = new GridData(GridData.FILL_HORIZONTAL);
        gd.horizontalSpan = 3;
        mUseDefaultLocation.setLayoutData(gd);
        mUseDefaultLocation.setText("Use default location");
        mUseDefaultLocation.setSelection(INITIAL_USE_DEFAULT_LOCATION);

        mUseDefaultLocation.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                super.widgetSelected(e);
                enableLocationWidgets();
                validatePageComplete();
            }
        });


        mLocationLabel = new Label(group, SWT.NONE);
        mLocationLabel.setText("Location:");

        mLocationPathField = new Text(group, SWT.BORDER);
        GridData data = new GridData(GridData.FILL, /* horizontal alignment */
                GridData.BEGINNING, /* vertical alignment */
                true,  /* grabExcessHorizontalSpace */
                false, /* grabExcessVerticalSpace */
                1,     /* horizontalSpan */
                1);    /* verticalSpan */
        mLocationPathField.setLayoutData(data);
        mLocationPathField.setFont(parent.getFont());
        mLocationPathField.addListener(SWT.Modify, new Listener() {
           public void handleEvent(Event event) {
               onLocationPathFieldModified();
            }
        });

        mBrowseButton = new Button(group, SWT.PUSH);
        mBrowseButton.setText("Browse...");
        setButtonLayoutData(mBrowseButton);
        mBrowseButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                onOpenDirectoryBrowser();
            }
        });
    }

    /**
     * Creates the group for Test Target options.
     *
     * There are two different modes here:
     * <ul>
     * <li>When mMainInfo exists, this is part of a new Android Project. In which case
     * the new test is tied to the soon-to-be main project and there is actually no choice.
     * <li>When mMainInfo does not exist, this is a standalone new test project. In this case
     * we offer 2 options for the test target: self test or against an existing Android project.
     * </ul>
     *
     * @param parent the parent composite
     */
    private final void createTestTargetGroup(Composite parent) {

        Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
        GridLayout layout = new GridLayout();
        layout.numColumns = 3;
        group.setLayout(layout);
        group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        group.setFont(parent.getFont());
        group.setText("Test Target");

        mToggleComposites.add(group);

        if (mMainInfo == null) {
            // Standalone mode: choose between self-test and existing-project test

            Label label = new Label(group, SWT.NONE);
            label.setText("Select the project to test:");
            GridData gd = new GridData(GridData.FILL_HORIZONTAL);
            gd.horizontalSpan = 3;
            label.setLayoutData(gd);

            mTestSelfProjectRadio = new Button(group, SWT.RADIO);
            mTestSelfProjectRadio.setText("This project");
            mTestSelfProjectRadio.setSelection(false);
            gd = new GridData(GridData.FILL_HORIZONTAL);
            gd.horizontalSpan = 3;
            mTestSelfProjectRadio.setLayoutData(gd);

            mTestExistingProjectRadio = new Button(group, SWT.RADIO);
            mTestExistingProjectRadio.setText("An existing Android project");
            mTestExistingProjectRadio.setSelection(mMainInfo == null);
            mTestExistingProjectRadio.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    onExistingProjectChanged();
                }
            });

            String tooltip = "The existing Android Project that is being tested.";

            mTestedProjectNameField = new Text(group, SWT.BORDER);
            mTestedProjectNameField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
            mTestedProjectNameField.setToolTipText(tooltip);
            mTestedProjectNameField.addModifyListener(new ModifyListener() {
                public void modifyText(ModifyEvent e) {
                    onProjectFieldUpdated();
                }
            });

            mProjectBrowseButton = new Button(group, SWT.NONE);
            mProjectBrowseButton.setText("Browse...");
            mProjectBrowseButton.setToolTipText("Allows you to select the Android project to test.");
            mProjectBrowseButton.addSelectionListener(new SelectionAdapter() {
               @Override
                public void widgetSelected(SelectionEvent e) {
                   onProjectBrowse();
                }
            });

            mProjectChooserHelper = new ProjectChooserHelper(parent.getShell(), null /*filter*/);
        } else {
            // Part of NPW mode: no selection.

        }

        // package label line

        Label label = new Label(group, SWT.NONE);
        label.setText("Test Target Package:");
        mTestTargetPackageLabel = new Label(group, SWT.NONE);
        GridData gd = new GridData(GridData.FILL_HORIZONTAL);
        gd.horizontalSpan = 2;
        mTestTargetPackageLabel.setLayoutData(gd);
    }

    /**
     * Creates the target group.
     * It only contains an SdkTargetSelector.
     */
    private void createTargetGroup(Composite parent) {
        Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
        // Layout has 1 column
        group.setLayout(new GridLayout());
        group.setLayoutData(new GridData(GridData.FILL_BOTH));
        group.setFont(parent.getFont());
        group.setText("Build Target");

        mToggleComposites.add(group);

        // The selector is created without targets. They are added below in the change listener.
        mSdkTargetSelector = new SdkTargetSelector(group, null);

        mSdkTargetChangeListener = new ITargetChangeListener() {
            public void onSdkLoaded() {
                // Update the sdk target selector with the new targets

                // get the targets from the sdk
                IAndroidTarget[] targets = null;
                if (Sdk.getCurrent() != null) {
                    targets = Sdk.getCurrent().getTargets();
                }
                mSdkTargetSelector.setTargets(targets);

                // If there's only one target, select it
                if (targets != null && targets.length == 1) {
                    mSdkTargetSelector.setSelection(targets[0]);
                }
            }

            public void onProjectTargetChange(IProject changedProject) {
                // Ignore
            }

            public void onTargetLoaded(IAndroidTarget target) {
                // Ignore
            }
        };

        AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener);

        // Invoke it once to initialize the targets
        mSdkTargetChangeListener.onSdkLoaded();

        mSdkTargetSelector.setSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                onSdkTargetModified();
                updateLocationPathField(null);
                validatePageComplete();
            }
        });
    }

    /**
     * Creates the group for the project properties:
     * - Package name [text field]
     * - Activity name [text field]
     * - Application name [text field]
     *
     * @param parent the parent composite
     */
    private final void createPropertiesGroup(Composite parent) {
        // package specification group
        Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
        GridLayout layout = new GridLayout();
        layout.numColumns = 2;
        group.setLayout(layout);
        group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        group.setFont(parent.getFont());
        group.setText("Properties");

        mToggleComposites.add(group);

        // new application label
        Label label = new Label(group, SWT.NONE);
        label.setText("Application name:");
        label.setFont(parent.getFont());
        label.setToolTipText("Name of the Application. This is a free string. It can be empty.");

        // new application name entry field
        mApplicationNameField = new Text(group, SWT.BORDER);
        GridData data = new GridData(GridData.FILL_HORIZONTAL);
        mApplicationNameField.setToolTipText("Name of the Application. This is a free string. It can be empty.");
        mApplicationNameField.setLayoutData(data);
        mApplicationNameField.setFont(parent.getFont());
        mApplicationNameField.addListener(SWT.Modify, new Listener() {
           public void handleEvent(Event event) {
               if (!mInternalApplicationNameUpdate) {
                   mApplicationNameModifiedByUser = true;
               }
           }
        });

        // new package label
        label = new Label(group, SWT.NONE);
        label.setText("Package name:");
        label.setFont(parent.getFont());
        label.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components.");

        // new package name entry field
        mPackageNameField = new Text(group, SWT.BORDER);
        data = new GridData(GridData.FILL_HORIZONTAL);
        mPackageNameField.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components.");
        mPackageNameField.setLayoutData(data);
        mPackageNameField.setFont(parent.getFont());
        mPackageNameField.addListener(SWT.Modify, new Listener() {
            public void handleEvent(Event event) {
                if (!mInternalPackageNameUpdate) {
                    mPackageNameModifiedByUser = true;
                }
                onPackageNameFieldModified();
            }
        });

        // min sdk version label
        label = new Label(group, SWT.NONE);
        label.setText("Min SDK Version:");
        label.setFont(parent.getFont());
        label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty.");

        // min sdk version entry field
        mMinSdkVersionField = new Text(group, SWT.BORDER);
        data = new GridData(GridData.FILL_HORIZONTAL);
        label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty.");
        mMinSdkVersionField.setLayoutData(data);
        mMinSdkVersionField.setFont(parent.getFont());
        mMinSdkVersionField.addListener(SWT.Modify, new Listener() {
            public void handleEvent(Event event) {
                onMinSdkVersionFieldModified();
                validatePageComplete();
            }
        });
    }

    private void createWorkingSetGroup(final Composite composite) {
        Composite group = mWorkingSetGroup.createControl(composite);
        group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        mToggleComposites.add(group);
    }

    //--- Internal getters & setters ------------------

    /** Returns the location path field value with spaces trimmed. */
    private String getLocationPathFieldValue() {
        return mLocationPathField == null ? "" : mLocationPathField.getText().trim();  //$NON-NLS-1$
    }

    /** Returns the current project location, depending on the Use Default Location check box. */
    private String getProjectLocation() {
        if (mInfo.useDefaultLocation()) {
            return Platform.getLocation().toString();
        } else {
            return getLocationPathFieldValue();
        }
    }

    /**
     * Creates a project resource handle for the current project name field
     * value.
     * <p>
     * This method does not create the project resource; this is the
     * responsibility of <code>IProject::create</code> invoked by the new
     * project resource wizard.
     * </p>
     *
     * @return the new project resource handle
     */
    private IProject getProjectHandle() {
        return ResourcesPlugin.getWorkspace().getRoot().getProject(mInfo.getProjectName());
    }

    // --- UI Callbacks ----

    /**
     * Callback invoked when the user toggles the "Test target: Existing Android Project"
     * checkbox. It enables or disable the UI to select an existing project.
     */
    private void onExistingProjectChanged() {
        if (mInfo.isTestingExisting()) {
            boolean enabled = mTestExistingProjectRadio.getSelection();
            mTestedProjectNameField.setEnabled(enabled);
            mProjectBrowseButton.setEnabled(enabled);
            setExistingProject(mInfo.getExistingTestedProject());
            validatePageComplete();
        }
    }

    /**
     * Tries to load the defaults from the main page if possible.
     */
    private void useMainProjectInformation() {
        if (mInfo.isTestingMain() && mMainInfo != null) {
            useMainWorkingSets();

            String projName = String.format("%1$sTest", mMainInfo.getProjectName());
            String appName = String.format("%1$sTest", mMainInfo.getApplicationName());

            String packageName = mMainInfo.getPackageName();
            if (packageName == null) {
                packageName = "";  //$NON-NLS-1$
            }

            updateTestTargetPackageField(packageName);

            if (!mProjectNameModifiedByUser) {
                mInternalProjectNameUpdate = true;
                mProjectNameField.setText(projName);  //$NON-NLS-1$
                mInternalProjectNameUpdate = false;
            }

            if (!mApplicationNameModifiedByUser) {
                mInternalApplicationNameUpdate = true;
                mApplicationNameField.setText(appName);
                mInternalApplicationNameUpdate = false;
            }

            if (!mPackageNameModifiedByUser) {
                mInternalPackageNameUpdate = true;
                packageName += ".test";  //$NON-NLS-1$
                mPackageNameField.setText(packageName);
                mInternalPackageNameUpdate = false;
            }

            if (!mSdkTargetModifiedByUser) {
                mInternalSdkTargetUpdate = true;
                mSdkTargetSelector.setSelection(mMainInfo.getSdkTarget());
                mInternalSdkTargetUpdate = false;
            }

            if (!mMinSdkVersionModifiedByUser) {
                mInternalMinSdkVersionUpdate = true;
                mMinSdkVersionField.setText(mMainInfo.getMinSdkVersion());
                mInternalMinSdkVersionUpdate = false;
            }
        }
    }

    private void useMainWorkingSets() {
        IWorkingSet[] workingSets = mMainInfo.getSelectedWorkingSets();
        if (workingSets != null) {
            // getSelectedWorkingSets returns an empty list if the working set feature is disabled.
            if (workingSets.length > 0) {
                mWorkingSetGroup.setChecked(true);
            }
            mWorkingSetGroup.setWorkingSets(workingSets);
        }
    }

    /**
     * Callback invoked when the user edits the project text field.
     */
    private void onProjectFieldUpdated() {
        String project = mTestedProjectNameField.getText();

        // Is this a valid project?
        IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null /*javaModel*/);
        for (IJavaProject p : projects) {
            if (p.getProject().getName().equals(project)) {
                setExistingProject(p.getProject());
                return;
            }
        }
    }

    /**
     * Callback called when the user uses the "Browse Projects" button.
     */
    private void onProjectBrowse() {
        IJavaProject p = mProjectChooserHelper.chooseJavaProject(mTestedProjectNameField.getText(),
                null /*message*/);
        if (p != null) {
            setExistingProject(p.getProject());
            mTestedProjectNameField.setText(mExistingTestedProject.getName());
        }
    }

    private void setExistingProject(IProject project) {
        mExistingTestedProject = project;

        // Try to update the application, package, sdk target and minSdkVersion accordingly
        if (project != null &&
                (!mApplicationNameModifiedByUser ||
                 !mPackageNameModifiedByUser     ||
                 !mSdkTargetModifiedByUser       ||
                 !mMinSdkVersionModifiedByUser)) {

            ManifestData manifestData = AndroidManifestHelper.parseForData(project);
            if (manifestData != null) {
                String appName = String.format("%1$sTest", project.getName());
                String packageName = manifestData.getPackage();
                String minSdkVersion = manifestData.getMinSdkVersionString();
                IAndroidTarget sdkTarget = null;
                if (Sdk.getCurrent() != null) {
                    sdkTarget = Sdk.getCurrent().getTarget(project);
                }

                if (packageName == null) {
                    packageName = "";  //$NON-NLS-1$
                }
                mLastExistingPackageName = packageName;

                if (!mProjectNameModifiedByUser) {
                    mInternalProjectNameUpdate = true;
                    mProjectNameField.setText(appName);
                    mInternalProjectNameUpdate = false;
                }

                if (!mApplicationNameModifiedByUser) {
                    mInternalApplicationNameUpdate = true;
                    mApplicationNameField.setText(appName);
                    mInternalApplicationNameUpdate = false;
                }

                if (!mPackageNameModifiedByUser) {
                    mInternalPackageNameUpdate = true;
                    packageName += ".test";  //$NON-NLS-1$
                    mPackageNameField.setText(packageName);  //$NON-NLS-1$
                    mInternalPackageNameUpdate = false;
                }

                if (!mSdkTargetModifiedByUser && sdkTarget != null) {
                    mInternalSdkTargetUpdate = true;
                    mSdkTargetSelector.setSelection(sdkTarget);
                    mInternalSdkTargetUpdate = false;
                }

                if (!mMinSdkVersionModifiedByUser) {
                    mInternalMinSdkVersionUpdate = true;
                    if (minSdkVersion != null) {
                        mMinSdkVersionField.setText(minSdkVersion);
                    }
                    if (sdkTarget == null) {
                        updateSdkSelectorToMatchMinSdkVersion();
                    }
                    mInternalMinSdkVersionUpdate = false;
                }
            }
        }

        updateTestTargetPackageField(mLastExistingPackageName);
        validatePageComplete();
    }

    /**
     * Display a directory browser and update the location path field with the selected path
     */
    private void onOpenDirectoryBrowser() {

        String existing_dir = getLocationPathFieldValue();

        // Disable the path if it doesn't exist
        if (existing_dir.length() == 0) {
            existing_dir = null;
        } else {
            File f = new File(existing_dir);
            if (!f.exists()) {
                existing_dir = null;
            }
        }

        DirectoryDialog dd = new DirectoryDialog(mLocationPathField.getShell());
        dd.setMessage("Browse for folder");
        dd.setFilterPath(existing_dir);
        String abs_dir = dd.open();

        if (abs_dir != null) {
            updateLocationPathField(abs_dir);
            validatePageComplete();
        }
    }

    /**
     * Callback when the "create test project" checkbox is changed.
     * It enables or disables all UI groups accordingly.
     */
    private void onCreateTestProjectToggle() {
        boolean enabled = mInfo.getCreateTestProject();
        for (Composite c : mToggleComposites) {
            enableControl(c, enabled);
        }
        mSdkTargetSelector.setEnabled(enabled);

        if (enabled) {
            useMainProjectInformation();
        }
        validatePageComplete();
    }

    /** Enables or disables controls; recursive for composite controls. */
    private void enableControl(Control c, boolean enabled) {
        c.setEnabled(enabled);
        if (c instanceof Composite)
        for (Control c2 : ((Composite) c).getChildren()) {
            enableControl(c2, enabled);
        }
    }

    /**
     * Enables or disable the location widgets depending on the user selection:
     * the location path is enabled when using the "existing source" mode (i.e. not new project)
     * or in new project mode with the "use default location" turned off.
     */
    private void enableLocationWidgets() {
        boolean use_default = mInfo.useDefaultLocation();
        boolean location_enabled = !use_default;

        mLocationLabel.setEnabled(location_enabled);
        mLocationPathField.setEnabled(location_enabled);
        mBrowseButton.setEnabled(location_enabled);

        updateLocationPathField(null);
    }

    /**
     * Updates the location directory path field.
     * <br/>
     * When custom user selection is enabled, use the abs_dir argument if not null and also
     * save it internally. If abs_dir is null, restore the last saved abs_dir. This allows the
     * user selection to be remembered when the user switches from default to custom.
     * <br/>
     * When custom user selection is disabled, use the workspace default location with the
     * current project name. This does not change the internally cached abs_dir.
     *
     * @param abs_dir A new absolute directory path or null to use the default.
     */
    private void updateLocationPathField(String abs_dir) {
        boolean use_default = mInfo.useDefaultLocation();
        boolean custom_location = !use_default;

        if (!mInternalLocationPathUpdate) {
            mInternalLocationPathUpdate = true;
            if (custom_location) {
                if (abs_dir != null) {
                    // We get here if the user selected a directory with the "Browse" button.
                    sCustomLocationOsPath = TextProcessor.process(abs_dir);
                }
                if (!mLocationPathField.getText().equals(sCustomLocationOsPath)) {
                    mLocationPathField.setText(sCustomLocationOsPath);
                }
            } else {
                String value = Platform.getLocation().append(mInfo.getProjectName()).toString();
                value = TextProcessor.process(value);
                if (!mLocationPathField.getText().equals(value)) {
                    mLocationPathField.setText(value);
                }
            }
            validatePageComplete();
            mInternalLocationPathUpdate = false;
        }
    }

    /**
     * The location path field is either modified internally (from updateLocationPathField)
     * or manually by the user when the custom_location mode is not set.
     *
     * Ignore the internal modification. When modified by the user, memorize the choice and
     * validate the page.
     */
    private void onLocationPathFieldModified() {
        if (!mInternalLocationPathUpdate) {
            // When the updates doesn't come from updateLocationPathField, it must be the user
            // editing the field manually, in which case we want to save the value internally
            String newPath = getLocationPathFieldValue();
            sCustomLocationOsPath = newPath;
            validatePageComplete();
        }
    }

    /**
     * The package name field is either modified internally (from extractNamesFromAndroidManifest)
     * or manually by the user when the custom_location mode is not set.
     *
     * Ignore the internal modification. When modified by the user, memorize the choice and
     * validate the page.
     */
    private void onPackageNameFieldModified() {
        updateTestTargetPackageField(null);
        validatePageComplete();
    }

    /**
     * Changes the {@link #mTestTargetPackageLabel} field.
     *
     * When using the "self-test" option, the packageName argument is ignored and the
     * current value from the project package is used.
     *
     * Otherwise the packageName is used if it is not null.
     */
    private void updateTestTargetPackageField(String packageName) {
        if (mInfo.isTestingSelf()) {
            mTestTargetPackageLabel.setText(mInfo.getPackageName());

        } else if (packageName != null) {
            mTestTargetPackageLabel.setText(packageName);
        }
    }

    /**
     * Called when the min sdk version field has been modified.
     *
     * Ignore the internal modifications. When modified by the user, try to match
     * a target with the same API level.
     */
    private void onMinSdkVersionFieldModified() {
        if (mInternalMinSdkVersionUpdate || mInternalSdkTargetUpdate) {
            return;
        }

        updateSdkSelectorToMatchMinSdkVersion();

        mMinSdkVersionModifiedByUser = true;
    }

    /**
     * Try to find an SDK Target that matches the current MinSdkVersion.
     *
     * There can be multiple targets with the same sdk api version, so don't change
     * it if it's already at the right version. Otherwise pick the first target
     * that matches.
     */
    private void updateSdkSelectorToMatchMinSdkVersion() {
        String minSdkVersion = mInfo.getMinSdkVersion();

        IAndroidTarget curr_target = mInfo.getSdkTarget();
        if (curr_target != null && curr_target.getVersion().equals(minSdkVersion)) {
            return;
        }

        for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
            if (target.getVersion().equals(minSdkVersion)) {
                mSdkTargetSelector.setSelection(target);
                return;
            }
        }
    }

    /**
     * Called when an SDK target is modified.
     *
     * Also changes the minSdkVersion field to reflect the sdk api level that has
     * just been selected.
     */
    private void onSdkTargetModified() {
        if (mInternalMinSdkVersionUpdate || mInternalSdkTargetUpdate) {
            return;
        }

        IAndroidTarget target = mInfo.getSdkTarget();

        if (target != null) {
            mInternalMinSdkVersionUpdate = true;
            mMinSdkVersionField.setText(target.getVersion().getApiString());
            mInternalMinSdkVersionUpdate = false;
        }

        mSdkTargetModifiedByUser = true;
    }

    /**
     * Returns whether this page's controls currently all contain valid values.
     *
     * @return <code>true</code> if all controls are valid, and
     *         <code>false</code> if at least one is invalid
     */
    private boolean validatePage() {
        IWorkspace workspace = ResourcesPlugin.getWorkspace();

        int status = MSG_NONE;

        // there is nothing to validate if we're not going to create a test project
        if (mInfo.getCreateTestProject()) {
            status = validateProjectField(workspace);
            if ((status & MSG_ERROR) == 0) {
                status |= validateLocationPath(workspace);
            }
            if ((status & MSG_ERROR) == 0) {
                status |= validateTestTarget();
            }
            if ((status & MSG_ERROR) == 0) {
                status |= validateSdkTarget();
            }
            if ((status & MSG_ERROR) == 0) {
                status |= validatePackageField();
            }
            if ((status & MSG_ERROR) == 0) {
                status |= validateMinSdkVersionField();
            }
        }
        if (status == MSG_NONE)  {
            setStatus(null, MSG_NONE);
        }

        // Return false if there's an error so that the finish button be disabled.
        return (status & MSG_ERROR) == 0;
    }

    /**
     * Validates the page and updates the Next/Finish buttons
     */
    private void validatePageComplete() {
        setPageComplete(validatePage());
    }

    /**
     * Validates the test target (self, main project or existing project)
     *
     * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
     */
    private int validateTestTarget() {
        if (mInfo.isTestingExisting() && mInfo.getExistingTestedProject() == null) {
            return setStatus("Please select an existing Android project as a test target.",
                    MSG_ERROR);
        }

        return MSG_NONE;
    }

    /**
     * Validates the project name field.
     *
     * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
     */
    private int validateProjectField(IWorkspace workspace) {
        // Validate project field
        String projectName = mInfo.getProjectName();
        if (projectName.length() == 0) {
            return setStatus("Project name must be specified", MSG_ERROR);
        }

        // Limit the project name to shell-agnostic characters since it will be used to
        // generate the final package
        if (!sProjectNamePattern.matcher(projectName).matches()) {
            return setStatus("The project name must start with an alphanumeric characters, followed by one or more alphanumerics, digits, dots, dashes, underscores or spaces.",
                    MSG_ERROR);
        }

        IStatus nameStatus = workspace.validateName(projectName, IResource.PROJECT);
        if (!nameStatus.isOK()) {
            return setStatus(nameStatus.getMessage(), MSG_ERROR);
        }

        if (mMainInfo != null && projectName.equals(mMainInfo.getProjectName())) {
            return setStatus("The main project name and the test project name must be different.",
                    MSG_ERROR);
        }

        if (getProjectHandle().exists()) {
            return setStatus("A project with that name already exists in the workspace",
                    MSG_ERROR);
        }

        return MSG_NONE;
    }

    /**
     * Validates the location path field.
     *
     * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
     */
    private int validateLocationPath(IWorkspace workspace) {
        Path path = new Path(getProjectLocation());
        if (!mInfo.useDefaultLocation()) {
            // If not using the default value validate the location.
            URI uri = URIUtil.toURI(path.toOSString());
            IStatus locationStatus = workspace.validateProjectLocationURI(getProjectHandle(),
                    uri);
            if (!locationStatus.isOK()) {
                return setStatus(locationStatus.getMessage(), MSG_ERROR);
            } else {
                // The location is valid as far as Eclipse is concerned (i.e. mostly not
                // an existing workspace project.) Check it either doesn't exist or is
                // a directory that is empty.
                File f = path.toFile();
                if (f.exists() && !f.isDirectory()) {
                    return setStatus("A directory name must be specified.", MSG_ERROR);
                } else if (f.isDirectory()) {
                    // However if the directory exists, we should put a warning if it is not
                    // empty. We don't put an error (we'll ask the user again for confirmation
                    // before using the directory.)
                    String[] l = f.list();
                    if (l.length != 0) {
                        return setStatus("The selected output directory is not empty.",
                                MSG_WARNING);
                    }
                }
            }
        } else {
            // Otherwise validate the path string is not empty
            if (getProjectLocation().length() == 0) {
                return setStatus("A directory name must be specified.", MSG_ERROR);
            }

            File dest = path.append(mInfo.getProjectName()).toFile();
            if (dest.exists()) {
                return setStatus(String.format("There is already a file or directory named \"%1$s\" in the selected location.",
                        mInfo.getProjectName()), MSG_ERROR);
            }
        }

        return MSG_NONE;
    }

    /**
     * Validates the sdk target choice.
     *
     * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
     */
    private int validateSdkTarget() {
        if (mInfo.getSdkTarget() == null) {
            return setStatus("An SDK Target must be specified.", MSG_ERROR);
        }
        return MSG_NONE;
    }

    /**
     * Validates the sdk target choice.
     *
     * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
     */
    private int validateMinSdkVersionField() {

        // If the min sdk version is empty, it is always accepted.
        if (mInfo.getMinSdkVersion().length() == 0) {
            return MSG_NONE;
        }

        if (mInfo.getSdkTarget() != null &&
                mInfo.getSdkTarget().getVersion().equals(mInfo.getMinSdkVersion()) == false) {
            return setStatus("The API level for the selected SDK target does not match the Min SDK version.",
                    mInfo.getSdkTarget().getVersion().isPreview() ? MSG_ERROR : MSG_WARNING);
        }

        return MSG_NONE;
    }

    /**
     * Validates the package name field.
     *
     * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
     */
    private int validatePackageField() {
        // Validate package field
        String packageName = mInfo.getPackageName();
        if (packageName.length() == 0) {
            return setStatus("Project package name must be specified.", MSG_ERROR);
        }

        // Check it's a valid package string
        int result = MSG_NONE;
        IStatus status = JavaConventions.validatePackageName(packageName, "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
        if (!status.isOK()) {
            result = setStatus(String.format("Project package: %s", status.getMessage()),
                        status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
        }

        // The Android Activity Manager does not accept packages names with only one
        // identifier. Check the package name has at least one dot in them (the previous rule
        // validated that if such a dot exist, it's not the first nor last characters of the
        // string.)
        if (result != MSG_ERROR && packageName.indexOf('.') == -1) {
            return setStatus("Project package name must have at least two identifiers.", MSG_ERROR);
        }

        // Check that the target package name is valid too
        packageName = mInfo.getTargetPackageName();
        if (packageName.length() == 0) {
            return setStatus("Target package name must be specified.", MSG_ERROR);
        }

        // Check it's a valid package string
        status = JavaConventions.validatePackageName(packageName, "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
        if (!status.isOK()) {
            result = setStatus(String.format("Target package: %s", status.getMessage()),
                        status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
        }

        if (result != MSG_ERROR && packageName.indexOf('.') == -1) {
            return setStatus("Target name must have at least two identifiers.", MSG_ERROR);
        }

        return result;
    }

    /**
     * Sets the error message for the wizard with the given message icon.
     *
     * @param message The wizard message type, one of MSG_ERROR or MSG_WARNING.
     * @return As a convenience, always returns messageType so that the caller can return
     *         immediately.
     */
    private int setStatus(String message, int messageType) {
        if (message == null) {
            setErrorMessage(null);
            setMessage(null);
        } else if (!message.equals(getMessage())) {
            setMessage(message, messageType == MSG_WARNING ? WizardPage.WARNING : WizardPage.ERROR);
        }
        return messageType;
    }

    /**
     * Returns the working sets to which the new project should be added.
     *
     * @return the selected working sets to which the new project should be added
     */
    public IWorkingSet[] getWorkingSets() {
        return mWorkingSetGroup.getSelectedWorkingSets();
    }

    /**
     * Sets the working sets to which the new project should be added.
     *
     * @param workingSets the initial selected working sets
     */
    public void setWorkingSets(IWorkingSet[] workingSets) {
        assert workingSets != null;
        mWorkingSetGroup.setWorkingSets(workingSets);
    }


}
