/*
 * 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.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
import com.android.ddmlib.Client;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IDevice.DeviceState;
import com.android.ddmuilib.ImageLoader;
import com.android.ddmuilib.TableHelper;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.sdk.AdtConsoleSdkLog;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.ide.eclipse.ddms.DdmsPlugin;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.internal.avd.AvdInfo;
import com.android.sdkuilib.internal.widgets.AvdSelector;
import com.android.sdkuilib.internal.widgets.AvdSelector.DisplayMode;
import com.android.sdkuilib.internal.widgets.AvdSelector.IAvdFilter;

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
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.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;

/**
 * A dialog that lets the user choose a device to deploy an application.
 * The user can either choose an exiting running device (including running emulators)
 * or start a new emulator using an Android Virtual Device configuration that matches
 * the current project.
 */
public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener {

    private final static int ICON_WIDTH = 16;

    private Table mDeviceTable;
    private TableViewer mViewer;
    private AvdSelector mPreferredAvdSelector;

    private Image mDeviceImage;
    private Image mEmulatorImage;
    private Image mMatchImage;
    private Image mNoMatchImage;
    private Image mWarningImage;

    private final DeviceChooserResponse mResponse;
    private final String mPackageName;
    private final IAndroidTarget mProjectTarget;
    private final Sdk mSdk;

    private Button mDeviceRadioButton;

    private boolean mDisableAvdSelectionChange = false;

    /**
     * Basic Content Provider for a table full of {@link IDevice} objects. The input is
     * a {@link AndroidDebugBridge}.
     */
    private static class ContentProvider implements IStructuredContentProvider {
        public Object[] getElements(Object inputElement) {
            if (inputElement instanceof AndroidDebugBridge) {
                return ((AndroidDebugBridge)inputElement).getDevices();
            }

            return new Object[0];
        }

        public void dispose() {
            // pass
        }

        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
            // pass
        }
    }


    /**
     * A Label Provider for the {@link TableViewer} in {@link DeviceChooserDialog}.
     * It provides labels and images for {@link IDevice} objects.
     */
    private class LabelProvider implements ITableLabelProvider {

        public Image getColumnImage(Object element, int columnIndex) {
            if (element instanceof IDevice) {
                IDevice device = (IDevice)element;
                switch (columnIndex) {
                    case 0:
                        return device.isEmulator() ? mEmulatorImage : mDeviceImage;

                    case 2:
                        // check for compatibility.
                        if (device.isEmulator() == false) { // physical device
                            // get the version of the device
                            AndroidVersion deviceVersion = Sdk.getDeviceVersion(device);
                            if (deviceVersion == null) {
                                return mWarningImage;
                            } else {
                                if (deviceVersion.canRun(mProjectTarget.getVersion()) == false) {
                                    return mNoMatchImage;
                                }

                                // if the project is compiling against an add-on,
                                // the optional API may be missing from the device.
                                return mProjectTarget.isPlatform() ?
                                        mMatchImage : mWarningImage;
                            }
                        } else {
                            // get the AvdInfo
                            AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(),
                                    true /*validAvdOnly*/);
                            if (info == null) {
                                return mWarningImage;
                            }
                            return mProjectTarget.canRunOn(info.getTarget()) ?
                                    mMatchImage : mNoMatchImage;
                        }
                }
            }

            return null;
        }

        public String getColumnText(Object element, int columnIndex) {
            if (element instanceof IDevice) {
                IDevice device = (IDevice)element;
                switch (columnIndex) {
                    case 0:
                        return device.getSerialNumber();
                    case 1:
                        if (device.isEmulator()) {
                            return device.getAvdName();
                        } else {
                            return "N/A"; // devices don't have AVD names.
                        }
                    case 2:
                        if (device.isEmulator()) {
                            AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(),
                                    true /*validAvdOnly*/);
                            if (info == null) {
                                return "?";
                            }
                            return info.getTarget().getFullName();
                        } else {
                            String deviceBuild = device.getProperty(IDevice.PROP_BUILD_VERSION);
                            if (deviceBuild == null) {
                                return "unknown";
                            }
                            return deviceBuild;
                        }
                    case 3:
                        String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE);
                        if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$
                            return "Yes";
                        } else {
                            return "";
                        }
                    case 4:
                        return getStateString(device);
                }
            }

            return null;
        }

        public void addListener(ILabelProviderListener listener) {
            // pass
        }

        public void dispose() {
            // pass
        }

        public boolean isLabelProperty(Object element, String property) {
            // pass
            return false;
        }

        public void removeListener(ILabelProviderListener listener) {
            // pass
        }
    }

    public static class DeviceChooserResponse {
        private AvdInfo mAvdToLaunch;
        private IDevice mDeviceToUse;

        public void setDeviceToUse(IDevice d) {
            mDeviceToUse = d;
            mAvdToLaunch = null;
        }

        public void setAvdToLaunch(AvdInfo avd) {
            mAvdToLaunch = avd;
            mDeviceToUse = null;
        }

        public IDevice getDeviceToUse() {
            return mDeviceToUse;
        }

        public AvdInfo getAvdToLaunch() {
            return mAvdToLaunch;
        }
    }

    public DeviceChooserDialog(Shell parent, DeviceChooserResponse response, String packageName,
            IAndroidTarget projectTarget) {
        super(parent);
        mResponse = response;
        mPackageName = packageName;
        mProjectTarget = projectTarget;
        mSdk = Sdk.getCurrent();

        AndroidDebugBridge.addDeviceChangeListener(this);
        loadImages();
    }

    private void cleanup() {
        // done listening.
        AndroidDebugBridge.removeDeviceChangeListener(this);
    }

    @Override
    protected void okPressed() {
        cleanup();
        super.okPressed();
    }

    @Override
    protected void cancelPressed() {
        cleanup();
        super.cancelPressed();
    }

    @Override
    protected Control createContents(Composite parent) {
        Control content = super.createContents(parent);

        // this must be called after createContents() has happened so that the
        // ok button has been created (it's created after the call to createDialogArea)
        updateDefaultSelection();

        return content;
    }


    @Override
    protected Control createDialogArea(Composite parent) {
        // set dialog title
        getShell().setText("Android Device Chooser");

        Composite top = new Composite(parent, SWT.NONE);
        top.setLayout(new GridLayout(1, true));

        Label label = new Label(top, SWT.NONE);
        label.setText(String.format("Select a device compatible with target %s.",
                mProjectTarget.getFullName()));

        mDeviceRadioButton = new Button(top, SWT.RADIO);
        mDeviceRadioButton.setText("Choose a running Android device");
        mDeviceRadioButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                boolean deviceMode = mDeviceRadioButton.getSelection();

                mDeviceTable.setEnabled(deviceMode);
                mPreferredAvdSelector.setEnabled(!deviceMode);

                if (deviceMode) {
                    handleDeviceSelection();
                } else {
                    mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected());
                }

                enableOkButton();
            }
        });
        mDeviceRadioButton.setSelection(true);


        // offset the selector from the radio button
        Composite offsetComp = new Composite(top, SWT.NONE);
        offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        GridLayout layout = new GridLayout(1, false);
        layout.marginRight = layout.marginHeight = 0;
        layout.marginLeft = 30;
        offsetComp.setLayout(layout);

        mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER);
        GridData gd;
        mDeviceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
        gd.heightHint = 100;

        mDeviceTable.setHeaderVisible(true);
        mDeviceTable.setLinesVisible(true);

        TableHelper.createTableColumn(mDeviceTable, "Serial Number",
                SWT.LEFT, "AAA+AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$
                null /* prefs name */, null /* prefs store */);

        TableHelper.createTableColumn(mDeviceTable, "AVD Name",
                SWT.LEFT, "AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$
                null /* prefs name */, null /* prefs store */);

        TableHelper.createTableColumn(mDeviceTable, "Target",
                SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$
                null /* prefs name */, null /* prefs store */);

        TableHelper.createTableColumn(mDeviceTable, "Debug",
                SWT.LEFT, "Debug", //$NON-NLS-1$
                null /* prefs name */, null /* prefs store */);

        TableHelper.createTableColumn(mDeviceTable, "State",
                SWT.LEFT, "bootloader", //$NON-NLS-1$
                null /* prefs name */, null /* prefs store */);

        // create the viewer for it
        mViewer = new TableViewer(mDeviceTable);
        mViewer.setContentProvider(new ContentProvider());
        mViewer.setLabelProvider(new LabelProvider());
        mViewer.setInput(AndroidDebugBridge.getBridge());

        mDeviceTable.addSelectionListener(new SelectionAdapter() {
            /**
             * Handles single-click selection on the device selector.
             * {@inheritDoc}
             */
            @Override
            public void widgetSelected(SelectionEvent e) {
                handleDeviceSelection();
            }

            /**
             * Handles double-click selection on the device selector.
             * Note that the single-click handler will probably already have been called.
             * {@inheritDoc}
             */
            @Override
            public void widgetDefaultSelected(SelectionEvent e) {
                handleDeviceSelection();
                if (isOkButtonEnabled()) {
                    okPressed();
                }
            }
        });

        Button radio2 = new Button(top, SWT.RADIO);
        radio2.setText("Launch a new Android Virtual Device");

        // offset the selector from the radio button
        offsetComp = new Composite(top, SWT.NONE);
        offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        layout = new GridLayout(1, false);
        layout.marginRight = layout.marginHeight = 0;
        layout.marginLeft = 30;
        offsetComp.setLayout(layout);

        mPreferredAvdSelector = new AvdSelector(offsetComp,
                mSdk.getSdkLocation(),
                mSdk.getAvdManager(),
                new NonRunningAvdFilter(),
                DisplayMode.SIMPLE_SELECTION,
                new AdtConsoleSdkLog());
        mPreferredAvdSelector.setTableHeightHint(100);
        mPreferredAvdSelector.setEnabled(false);
        mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() {
            /**
             * Handles single-click selection on the AVD selector.
             * {@inheritDoc}
             */
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (mDisableAvdSelectionChange == false) {
                    mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected());
                    enableOkButton();
                }
            }

            /**
             * Handles double-click selection on the AVD selector.
             *
             * Note that the single-click handler will probably already have been called
             * but the selected item can have changed in between.
             *
             * {@inheritDoc}
             */
            @Override
            public void widgetDefaultSelected(SelectionEvent e) {
                widgetSelected(e);
                if (isOkButtonEnabled()) {
                    okPressed();
                }
            }
        });


        return top;
    }

    private void loadImages() {
        ImageLoader ddmUiLibLoader = ImageLoader.getDdmUiLibLoader();
        Display display = DdmsPlugin.getDisplay();
        IconFactory factory = IconFactory.getInstance();

        if (mDeviceImage == null) {
            mDeviceImage = ddmUiLibLoader.loadImage(display,
                    "device.png", //$NON-NLS-1$
                    ICON_WIDTH, ICON_WIDTH,
                    display.getSystemColor(SWT.COLOR_RED));
        }
        if (mEmulatorImage == null) {
            mEmulatorImage = ddmUiLibLoader.loadImage(display,
                    "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
                    display.getSystemColor(SWT.COLOR_BLUE));
        }

        if (mMatchImage == null) {
            mMatchImage = factory.getIcon("match", //$NON-NLS-1$
                    IconFactory.COLOR_GREEN,
                    IconFactory.SHAPE_DEFAULT);
        }

        if (mNoMatchImage == null) {
            mNoMatchImage = factory.getIcon("error", //$NON-NLS-1$
                    IconFactory.COLOR_RED,
                    IconFactory.SHAPE_DEFAULT);
        }

        if (mWarningImage == null) {
            mWarningImage = factory.getIcon("warning", //$NON-NLS-1$
                    SWT.COLOR_YELLOW,
                    IconFactory.SHAPE_DEFAULT);
        }

    }

    /**
     * Returns a display string representing the state of the device.
     * @param d the device
     */
    private static String getStateString(IDevice d) {
        DeviceState deviceState = d.getState();
        if (deviceState == DeviceState.ONLINE) {
            return "Online";
        } else if (deviceState == DeviceState.OFFLINE) {
            return "Offline";
        } else if (deviceState == DeviceState.BOOTLOADER) {
            return "Bootloader";
        }

        return "??";
    }

    /**
     * Sent when the a device is connected to the {@link AndroidDebugBridge}.
     * <p/>
     * This is sent from a non UI thread.
     * @param device the new device.
     *
     * @see IDeviceChangeListener#deviceConnected(IDevice)
     */
    public void deviceConnected(IDevice device) {
        final DeviceChooserDialog dialog = this;
        exec(new Runnable() {
            public void run() {
                if (mDeviceTable.isDisposed() == false) {
                    // refresh all
                    mViewer.refresh();

                    // update the selection
                    updateDefaultSelection();

                    // update the display of AvdInfo (since it's filtered to only display
                    // non running AVD.)
                    refillAvdList(false /*reloadAvds*/);
                } else {
                    // table is disposed, we need to do something.
                    // lets remove ourselves from the listener.
                    AndroidDebugBridge.removeDeviceChangeListener(dialog);
                }

            }
        });
    }

    /**
     * Sent when the a device is connected to the {@link AndroidDebugBridge}.
     * <p/>
     * This is sent from a non UI thread.
     * @param device the new device.
     *
     * @see IDeviceChangeListener#deviceDisconnected(IDevice)
     */
    public void deviceDisconnected(IDevice device) {
        deviceConnected(device);
    }

    /**
     * Sent when a device data changed, or when clients are started/terminated on the device.
     * <p/>
     * This is sent from a non UI thread.
     * @param device the device that was updated.
     * @param changeMask the mask indicating what changed.
     *
     * @see IDeviceChangeListener#deviceChanged(IDevice, int)
     */
    public void deviceChanged(final IDevice device, int changeMask) {
        if ((changeMask & (IDevice.CHANGE_STATE | IDevice.CHANGE_BUILD_INFO)) != 0) {
            final DeviceChooserDialog dialog = this;
            exec(new Runnable() {
                public void run() {
                    if (mDeviceTable.isDisposed() == false) {
                        // refresh the device
                        mViewer.refresh(device);

                        // update the defaultSelection.
                        updateDefaultSelection();

                        // update the display of AvdInfo (since it's filtered to only display
                        // non running AVD). This is done on deviceChanged because the avd name
                        // of a (emulator) device may be updated as the emulator boots.

                        refillAvdList(false /*reloadAvds*/);

                        // if the changed device is the current selection,
                        // we update the OK button based on its state.
                        if (device == mResponse.getDeviceToUse()) {
                            enableOkButton();
                        }

                    } else {
                        // table is disposed, we need to do something.
                        // lets remove ourselves from the listener.
                        AndroidDebugBridge.removeDeviceChangeListener(dialog);
                    }
                }
            });
        }
    }

    /**
     * Returns whether the dialog is in "device" mode (true), or in "avd" mode (false).
     */
    private boolean isDeviceMode() {
        return mDeviceRadioButton.getSelection();
    }

    /**
     * Enables or disables the OK button of the dialog based on various selections in the dialog.
     */
    private void enableOkButton() {
        Button okButton = getButton(IDialogConstants.OK_ID);

        if (isDeviceMode()) {
            okButton.setEnabled(mResponse.getDeviceToUse() != null &&
                    mResponse.getDeviceToUse().isOnline());
        } else {
            okButton.setEnabled(mResponse.getAvdToLaunch() != null);
        }
    }

    /**
     * Returns true if the ok button is enabled.
     */
    private boolean isOkButtonEnabled() {
        Button okButton = getButton(IDialogConstants.OK_ID);
        return okButton.isEnabled();
    }

    /**
     * Executes the {@link Runnable} in the UI thread.
     * @param runnable the runnable to execute.
     */
    private void exec(Runnable runnable) {
        try {
            Display display = mDeviceTable.getDisplay();
            display.asyncExec(runnable);
        } catch (SWTException e) {
            // tree is disposed, we need to do something. lets remove ourselves from the listener.
            AndroidDebugBridge.removeDeviceChangeListener(this);
        }
    }

    private void handleDeviceSelection() {
        int count = mDeviceTable.getSelectionCount();
        if (count != 1) {
            handleSelection(null);
        } else {
            int index = mDeviceTable.getSelectionIndex();
            Object data = mViewer.getElementAt(index);
            if (data instanceof IDevice) {
                handleSelection((IDevice)data);
            } else {
                handleSelection(null);
            }
        }
    }

    private void handleSelection(IDevice device) {
        mResponse.setDeviceToUse(device);
        enableOkButton();
    }

    /**
     * Look for a default device to select. This is done by looking for the running
     * clients on each device and finding one similar to the one being launched.
     * <p/>
     * This is done every time the device list changed unless there is a already selection.
     */
    private void updateDefaultSelection() {
        if (mDeviceTable.getSelectionCount() == 0) {
            AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();

            IDevice[] devices = bridge.getDevices();

            for (IDevice device : devices) {
                Client[] clients = device.getClients();

                for (Client client : clients) {

                    if (mPackageName.equals(client.getClientData().getClientDescription())) {
                        // found a match! Select it.
                        mViewer.setSelection(new StructuredSelection(device));
                        handleSelection(device);

                        // and we're done.
                        return;
                    }
                }
            }
        }

        handleDeviceSelection();
    }

    private final class NonRunningAvdFilter implements IAvdFilter {

        private IDevice[] mDevices;

        public void prepare() {
            mDevices = AndroidDebugBridge.getBridge().getDevices();
        }

        public boolean accept(AvdInfo avd) {
            if (mDevices != null) {
                for (IDevice d : mDevices) {
                    if (mProjectTarget.canRunOn(avd.getTarget()) == false ||
                            avd.getName().equals(d.getAvdName())) {
                        return false;
                    }
                }
            }

            return true;
        }

        public void cleanup() {
            mDevices = null;
        }
    }

    /**
     * Refills the AVD list keeping the current selection.
     */
    private void refillAvdList(boolean reloadAvds) {
        // save the current selection
        AvdInfo selected = mPreferredAvdSelector.getSelected();

        // disable selection change.
        mDisableAvdSelectionChange = true;

        // refresh the list
        mPreferredAvdSelector.refresh(false);

        // attempt to reselect the proper avd if needed
        if (selected != null) {
            if (mPreferredAvdSelector.setSelection(selected) == false) {
                // looks like the selection is lost. this can happen if an emulator
                // running the AVD that was selected was launched from outside of Eclipse).
                mResponse.setAvdToLaunch(null);
                enableOkButton();
            }
        }

        // enable the selection change
        mDisableAvdSelectionChange = false;
    }
}

