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

package com.android.ide.eclipse.ddms;

import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.Client;
import com.android.ddmlib.DdmConstants;
import com.android.ddmlib.DdmPreferences;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
import com.android.ddmlib.Log.ILogOutput;
import com.android.ddmlib.Log.LogLevel;
import com.android.ddmuilib.DdmUiPreferences;
import com.android.ddmuilib.DevicePanel.IUiSelectionListener;
import com.android.ide.eclipse.ddms.preferences.PreferenceInitializer;
import com.android.ide.eclipse.ddms.views.DeviceView;

import org.eclipse.core.runtime.Preferences;
import org.eclipse.core.runtime.Preferences.IPropertyChangeListener;
import org.eclipse.core.runtime.Preferences.PropertyChangeEvent;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.console.MessageConsole;
import org.eclipse.ui.console.MessageConsoleStream;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;

import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;

/**
 * The activator class controls the plug-in life cycle
 */
public final class DdmsPlugin extends AbstractUIPlugin implements IDeviceChangeListener,
        IUiSelectionListener {


    // The plug-in ID
    public static final String PLUGIN_ID = "com.android.ide.eclipse.ddms"; // $NON-NLS-1$

    private static final String ADB_LOCATION = PLUGIN_ID + ".adb"; // $NON-NLS-1$

    /** The singleton instance */
    private static DdmsPlugin sPlugin;

    /** Location of the adb command line executable */
    private static String sAdbLocation;
    private static String sToolsFolder;
    private static String sHprofConverter;

    /**
     * Debug Launcher for already running apps
     */
    private static IDebugLauncher sRunningAppDebugLauncher;


    /** Console for DDMS log message */
    private MessageConsole mDdmsConsole;

    private IDevice mCurrentDevice;
    private Client mCurrentClient;
    private boolean mListeningToUiSelection = false;

    private final ArrayList<ISelectionListener> mListeners = new ArrayList<ISelectionListener>();

    private Color mRed;

    private boolean mDdmlibInitialized;

    /**
     * Interface to provide debugger launcher for running apps.
     */
    public interface IDebugLauncher {
        public boolean debug(String packageName, int port);
    }

    /**
     * Classes which implement this interface provide methods that deals
     * with {@link IDevice} and {@link Client} selectionchanges.
     */
    public interface ISelectionListener {

        /**
         * Sent when a new {@link Client} is selected.
         * @param selectedClient The selected client. If null, no clients are selected.
         */
        public void selectionChanged(Client selectedClient);

        /**
         * Sent when a new {@link IDevice} is selected.
         * @param selectedDevice the selected device. If null, no devices are selected.
         */
        public void selectionChanged(IDevice selectedDevice);
    }

    /**
     * The constructor
     */
    public DdmsPlugin() {
        sPlugin = this;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext)
     */
    @Override
    public void start(BundleContext context) throws Exception {
        super.start(context);

        final Display display = getDisplay();

        // get the eclipse store
        final IPreferenceStore eclipseStore = getPreferenceStore();

        AndroidDebugBridge.addDeviceChangeListener(this);

        DdmUiPreferences.setStore(eclipseStore);

        //DdmUiPreferences.displayCharts();

        // set the consoles.
        mDdmsConsole = new MessageConsole("DDMS", null); // $NON-NLS-1$
        ConsolePlugin.getDefault().getConsoleManager().addConsoles(
                new IConsole[] {
                    mDdmsConsole
                });

        final MessageConsoleStream consoleStream = mDdmsConsole.newMessageStream();
        final MessageConsoleStream errorConsoleStream = mDdmsConsole.newMessageStream();
        mRed = new Color(display, 0xFF, 0x00, 0x00);

        // because this can be run, in some cases, by a non UI thread, and because
        // changing the console properties update the UI, we need to make this change
        // in the UI thread.
        display.asyncExec(new Runnable() {
            public void run() {
                errorConsoleStream.setColor(mRed);
            }
        });

        // set up the ddms log to use the ddms console.
        Log.setLogOutput(new ILogOutput() {
            public void printLog(LogLevel logLevel, String tag, String message) {
                if (logLevel.getPriority() >= LogLevel.ERROR.getPriority()) {
                    printToStream(errorConsoleStream, tag, message);
                    ConsolePlugin.getDefault().getConsoleManager().showConsoleView(mDdmsConsole);
                } else {
                    printToStream(consoleStream, tag, message);
                }
            }

            public void printAndPromptLog(final LogLevel logLevel, final String tag,
                    final String message) {
                printLog(logLevel, tag, message);
                // dialog box only run in UI thread..
                display.asyncExec(new Runnable() {
                    public void run() {
                        Shell shell = display.getActiveShell();
                        if (logLevel == LogLevel.ERROR) {
                            MessageDialog.openError(shell, tag, message);
                        } else {
                            MessageDialog.openWarning(shell, tag, message);
                        }
                    }
                });
            }

        });

        // set the listener for the preference change
        Preferences prefs = getPluginPreferences();
        prefs.addPropertyChangeListener(new IPropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent event) {
                // get the name of the property that changed.
                String property = event.getProperty();

                if (PreferenceInitializer.ATTR_DEBUG_PORT_BASE.equals(property)) {
                    DdmPreferences.setDebugPortBase(
                            eclipseStore.getInt(PreferenceInitializer.ATTR_DEBUG_PORT_BASE));
                } else if (PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT.equals(property)) {
                    DdmPreferences.setSelectedDebugPort(
                            eclipseStore.getInt(PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT));
                } else if (PreferenceInitializer.ATTR_THREAD_INTERVAL.equals(property)) {
                    DdmUiPreferences.setThreadRefreshInterval(
                            eclipseStore.getInt(PreferenceInitializer.ATTR_THREAD_INTERVAL));
                } else if (PreferenceInitializer.ATTR_LOG_LEVEL.equals(property)) {
                    DdmPreferences.setLogLevel(
                            eclipseStore.getString(PreferenceInitializer.ATTR_LOG_LEVEL));
                } else if (PreferenceInitializer.ATTR_TIME_OUT.equals(property)) {
                    DdmPreferences.setTimeOut(
                            eclipseStore.getInt(PreferenceInitializer.ATTR_TIME_OUT));
                }
            }
        });

        // read the adb location from the prefs to attempt to start it properly without
        // having to wait for ADT to start
        final boolean adbValid = setAdbLocation(eclipseStore.getString(ADB_LOCATION));

        // start it in a thread to return from start() asap.
        new Thread() {
            @Override
            public void run() {
                // init ddmlib if needed
                getDefault().initDdmlib();

                // create and start the first bridge
                if (adbValid) {
                    AndroidDebugBridge.createBridge(sAdbLocation, true /* forceNewBridge */);
                }
            }
        }.start();
    }

    public static Display getDisplay() {
        IWorkbench bench = sPlugin.getWorkbench();
        if (bench != null) {
            return bench.getDisplay();
        }
        return null;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext)
     */
    @Override
    public void stop(BundleContext context) throws Exception {
        AndroidDebugBridge.removeDeviceChangeListener(this);

        AndroidDebugBridge.terminate();

        mRed.dispose();

        sPlugin = null;
        super.stop(context);
    }

    /**
     * Returns the shared instance
     *
     * @return the shared instance
     */
    public static DdmsPlugin getDefault() {
        return sPlugin;
    }

    public static String getAdb() {
        return sAdbLocation;
    }

    public static String getToolsFolder() {
        return sToolsFolder;
    }

    public static String getHprofConverter() {
        return sHprofConverter;
    }

    /**
     * Stores the adb location. This returns true if the location is an existing file.
     */
    private static boolean setAdbLocation(String adbLocation) {
        File adb = new File(adbLocation);
        if (adb.isFile()) {
            sAdbLocation = adbLocation;

            File toolsFolder = adb.getParentFile();
            sToolsFolder = toolsFolder.getAbsolutePath();

            File hprofConverter = new File(toolsFolder, DdmConstants.FN_HPROF_CONVERTER);
            sHprofConverter = hprofConverter.getAbsolutePath();

            File traceview = new File(toolsFolder, DdmConstants.FN_TRACEVIEW);
            DdmUiPreferences.setTraceviewLocation(traceview.getAbsolutePath());

            return true;
        }

        return false;
    }

    /**
     * Set the location of the adb executable and optionally starts adb
     * @param adb location of adb
     * @param startAdb flag to start adb
     */
    public static void setAdb(String adb, boolean startAdb) {
        if (adb != null) {
            if (setAdbLocation(adb)) {
                // store the location for future ddms only start.
                sPlugin.getPreferenceStore().setValue(ADB_LOCATION, sAdbLocation);

                // starts the server in a thread in case this is blocking.
                if (startAdb) {
                    new Thread() {
                        @Override
                        public void run() {
                            // init ddmlib if needed
                            getDefault().initDdmlib();

                            // create and start the bridge
                            AndroidDebugBridge.createBridge(sAdbLocation,
                                    false /* forceNewBridge */);
                        }
                    }.start();
                }
            }
        }
    }

    private synchronized void initDdmlib() {
        if (mDdmlibInitialized == false) {
            // set the preferences.
            PreferenceInitializer.setupPreferences();

            // init the lib
            AndroidDebugBridge.init(true /* debugger support */);

            mDdmlibInitialized = true;
        }
    }

    /**
     * Sets the launcher responsible for connecting the debugger to running applications.
     * @param launcher The launcher.
     */
    public static void setRunningAppDebugLauncher(IDebugLauncher launcher) {
        sRunningAppDebugLauncher = launcher;

        // if the process view is already running, give it the launcher.
        // This method could be called from a non ui thread, so we make sure to do that
        // in the ui thread.
        Display display = getDisplay();
        if (display != null && display.isDisposed() == false) {
            display.asyncExec(new Runnable() {
                public void run() {
                    DeviceView dv = DeviceView.getInstance();
                    if (dv != null) {
                        dv.setDebugLauncher(sRunningAppDebugLauncher);
                    }
                }
            });
        }
    }

    public static IDebugLauncher getRunningAppDebugLauncher() {
        return sRunningAppDebugLauncher;
    }

    public synchronized void addSelectionListener(ISelectionListener listener) {
        mListeners.add(listener);

        // notify the new listener of the current selection
       listener.selectionChanged(mCurrentDevice);
       listener.selectionChanged(mCurrentClient);
    }

    public synchronized void removeSelectionListener(ISelectionListener listener) {
        mListeners.remove(listener);
    }

    public synchronized void setListeningState(boolean state) {
        mListeningToUiSelection = state;
    }

    /**
     * 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) {
        // if we are listening to selection coming from the ui, then we do nothing, as
        // any change in the devices/clients, will be handled by the UI, and we'll receive
        // selection notification through our implementation of IUiSelectionListener.
        if (mListeningToUiSelection == false) {
            if (mCurrentDevice == null) {
                handleDefaultSelection(device);
            }
        }
    }

    /**
     * Sent when the a device is disconnected 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) {
        // if we are listening to selection coming from the ui, then we do nothing, as
        // any change in the devices/clients, will be handled by the UI, and we'll receive
        // selection notification through our implementation of IUiSelectionListener.
        if (mListeningToUiSelection == false) {
            // test if the disconnected device was the default selection.
            if (mCurrentDevice == device) {
                // try to find a new device
                AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
                if (bridge != null) {
                    // get the device list
                    IDevice[] devices = bridge.getDevices();

                    // check if we still have devices
                    if (devices.length == 0) {
                        handleDefaultSelection((IDevice)null);
                    } else {
                        handleDefaultSelection(devices[0]);
                    }
                } else {
                    handleDefaultSelection((IDevice)null);
                }
            }
        }
    }

    /**
     * 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)
     */
    public void deviceChanged(IDevice device, int changeMask) {
        // if we are listening to selection coming from the ui, then we do nothing, as
        // any change in the devices/clients, will be handled by the UI, and we'll receive
        // selection notification through our implementation of IUiSelectionListener.
        if (mListeningToUiSelection == false) {

            // check if this is our device
            if (device == mCurrentDevice) {
                if (mCurrentClient == null) {
                    handleDefaultSelection(device);
                } else {
                    // get the clients and make sure ours is still in there.
                    Client[] clients = device.getClients();
                    boolean foundClient = false;
                    for (Client client : clients) {
                        if (client == mCurrentClient) {
                            foundClient = true;
                            break;
                        }
                    }

                    // if we haven't found our client, lets look for a new one
                    if (foundClient == false) {
                        mCurrentClient = null;
                        handleDefaultSelection(device);
                    }
                }
            }
        }
    }

    /**
     * Sent when a new {@link IDevice} and {@link Client} are selected.
     * @param selectedDevice the selected device. If null, no devices are selected.
     * @param selectedClient The selected client. If null, no clients are selected.
     */
    public synchronized void selectionChanged(IDevice selectedDevice, Client selectedClient) {
        if (mCurrentDevice != selectedDevice) {
            mCurrentDevice = selectedDevice;

            // notify of the new default device
            for (ISelectionListener listener : mListeners) {
                listener.selectionChanged(mCurrentDevice);
            }
        }

        if (mCurrentClient != selectedClient) {
            mCurrentClient = selectedClient;

            // notify of the new default client
            for (ISelectionListener listener : mListeners) {
                listener.selectionChanged(mCurrentClient);
            }
        }
    }

    /**
     * Handles a default selection of a {@link IDevice} and {@link Client}.
     * @param device the selected device
     */
    private void handleDefaultSelection(final IDevice device) {
        // because the listener expect to receive this from the UI thread, and this is called
        // from the AndroidDebugBridge notifications, we need to run this in the UI thread.
        try {
            Display display = getDisplay();

            display.asyncExec(new Runnable() {
                public void run() {
                    // set the new device if different.
                    boolean newDevice = false;
                    if (mCurrentDevice != device) {
                        mCurrentDevice = device;
                        newDevice = true;

                        // notify of the new default device
                        for (ISelectionListener listener : mListeners) {
                            listener.selectionChanged(mCurrentDevice);
                        }
                    }

                    if (device != null) {
                        // if this is a device switch or the same device but we didn't find a valid
                        // client the last time, we go look for a client to use again.
                        if (newDevice || mCurrentClient == null) {
                            // now get the new client
                            Client[] clients =  device.getClients();
                            if (clients.length > 0) {
                                handleDefaultSelection(clients[0]);
                            } else {
                                handleDefaultSelection((Client)null);
                            }
                        }
                    } else {
                        handleDefaultSelection((Client)null);
                    }
                }
            });
        } catch (SWTException e) {
            // display is disposed. Do nothing since we're quitting anyway.
        }
    }

    private void handleDefaultSelection(Client client) {
        mCurrentClient = client;

        // notify of the new default client
        for (ISelectionListener listener : mListeners) {
            listener.selectionChanged(mCurrentClient);
        }
    }

    /**
     * Prints a message, associated with a project to the specified stream
     * @param stream The stream to write to
     * @param tag The tag associated to the message. Can be null
     * @param message The message to print.
     */
    private static synchronized void printToStream(MessageConsoleStream stream, String tag,
            String message) {
        String dateTag = getMessageTag(tag);

        stream.print(dateTag);
        stream.println(message);
    }

    /**
     * Creates a string containing the current date/time, and the tag
     * @param tag The tag associated to the message. Can be null
     * @return The dateTag
     */
    private static String getMessageTag(String tag) {
        Calendar c = Calendar.getInstance();

        if (tag == null) {
            return String.format("[%1$tF %1$tT]", c);
        }

        return String.format("[%1$tF %1$tT - %2$s]", c, tag);
    }
}
