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

import com.android.ddmlib.Client;
import com.android.ddmlib.ThreadInfo;
import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;

import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
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.custom.StackLayout;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
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.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Sash;
import org.eclipse.swt.widgets.Table;

import java.util.Date;

/**
 * Base class for our information panels.
 */
public class ThreadPanel extends TablePanel {
    
    private final static String PREFS_THREAD_COL_ID = "threadPanel.Col0"; //$NON-NLS-1$
    private final static String PREFS_THREAD_COL_TID = "threadPanel.Col1"; //$NON-NLS-1$
    private final static String PREFS_THREAD_COL_STATUS = "threadPanel.Col2"; //$NON-NLS-1$
    private final static String PREFS_THREAD_COL_UTIME = "threadPanel.Col3"; //$NON-NLS-1$
    private final static String PREFS_THREAD_COL_STIME = "threadPanel.Col4"; //$NON-NLS-1$
    private final static String PREFS_THREAD_COL_NAME = "threadPanel.Col5"; //$NON-NLS-1$
    
    private final static String PREFS_THREAD_SASH = "threadPanel.sash"; //$NON-NLS-1$

    private static final String PREFS_STACK_COL_CLASS = "threadPanel.stack.col0"; //$NON-NLS-1$
    private static final String PREFS_STACK_COL_METHOD = "threadPanel.stack.col1"; //$NON-NLS-1$
    private static final String PREFS_STACK_COL_FILE = "threadPanel.stack.col2"; //$NON-NLS-1$
    private static final String PREFS_STACK_COL_LINE = "threadPanel.stack.col3"; //$NON-NLS-1$
    private static final String PREFS_STACK_COL_NATIVE = "threadPanel.stack.col4"; //$NON-NLS-1$
    
    private Display mDisplay;
    private Composite mBase;
    private Label mNotEnabled;
    private Label mNotSelected;
    
    private Composite mThreadBase;
    private Table mThreadTable;
    private TableViewer mThreadViewer;

    private Composite mStackTraceBase;
    private Button mRefreshStackTraceButton;
    private Label mStackTraceTimeLabel;
    private StackTracePanel mStackTracePanel;
    private Table mStackTraceTable;

    /** Indicates if a timer-based Runnable is current requesting thread updates regularly. */
    private boolean mMustStopRecurringThreadUpdate = false;
    /** Flag to tell the recurring thread update to stop running */
    private boolean mRecurringThreadUpdateRunning = false;

    private Object mLock = new Object();

    private static final String[] THREAD_STATUS = {
        "zombie", "running", "timed-wait", "monitor",
        "wait", "init", "start", "native", "vmwait",
        "suspended"
    };
    
    /**
     * Content Provider to display the threads of a client.
     * Expected input is a {@link Client} object.
     */
    private static class ThreadContentProvider implements IStructuredContentProvider {
        public Object[] getElements(Object inputElement) {
            if (inputElement instanceof Client) {
                return ((Client)inputElement).getClientData().getThreads();
            }

            return new Object[0];
        }

        public void dispose() {
            // pass
        }

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

    /**
     * A Label Provider to use with {@link ThreadContentProvider}. It expects the elements to be
     * of type {@link ThreadInfo}.
     */
    private static class ThreadLabelProvider implements ITableLabelProvider {

        public Image getColumnImage(Object element, int columnIndex) {
            return null;
        }

        public String getColumnText(Object element, int columnIndex) {
            if (element instanceof ThreadInfo) {
                ThreadInfo thread = (ThreadInfo)element;
                switch (columnIndex) {
                    case 0:
                        return (thread.isDaemon() ? "*" : "") + //$NON-NLS-1$ //$NON-NLS-2$
                            String.valueOf(thread.getThreadId());
                    case 1:
                        return String.valueOf(thread.getTid());
                    case 2:
                        if (thread.getStatus() >= 0 && thread.getStatus() < THREAD_STATUS.length)
                            return THREAD_STATUS[thread.getStatus()];
                        return "unknown";
                    case 3:
                        return String.valueOf(thread.getUtime());
                    case 4:
                        return String.valueOf(thread.getStime());
                    case 5:
                        return thread.getThreadName();
                }
            }

            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
        }
    }

    /**
     * Create our control(s).
     */
    @Override
    protected Control createControl(Composite parent) {
        mDisplay = parent.getDisplay();

        final IPreferenceStore store = DdmUiPreferences.getStore();

        mBase = new Composite(parent, SWT.NONE);
        mBase.setLayout(new StackLayout());

        // UI for thread not enabled
        mNotEnabled = new Label(mBase, SWT.CENTER | SWT.WRAP);
        mNotEnabled.setText("Thread updates not enabled for selected client\n"
            + "(use toolbar button to enable)");

        // UI for not client selected
        mNotSelected = new Label(mBase, SWT.CENTER | SWT.WRAP);
        mNotSelected.setText("no client is selected");

        // base composite for selected client with enabled thread update.
        mThreadBase = new Composite(mBase, SWT.NONE);
        mThreadBase.setLayout(new FormLayout());
        
        // table above the sash
        mThreadTable = new Table(mThreadBase, SWT.MULTI | SWT.FULL_SELECTION);
        mThreadTable.setHeaderVisible(true);
        mThreadTable.setLinesVisible(true);

        TableHelper.createTableColumn(
                mThreadTable,
                "ID",
                SWT.RIGHT,
                "888", //$NON-NLS-1$
                PREFS_THREAD_COL_ID, store);

        TableHelper.createTableColumn(
                mThreadTable,
                "Tid",
                SWT.RIGHT,
                "88888", //$NON-NLS-1$
                PREFS_THREAD_COL_TID, store);

        TableHelper.createTableColumn(
                mThreadTable,
                "Status",
                SWT.LEFT,
                "timed-wait", //$NON-NLS-1$
                PREFS_THREAD_COL_STATUS, store);

        TableHelper.createTableColumn(
                mThreadTable,
                "utime",
                SWT.RIGHT,
                "utime", //$NON-NLS-1$
                PREFS_THREAD_COL_UTIME, store);

        TableHelper.createTableColumn(
                mThreadTable,
                "stime",
                SWT.RIGHT,
                "utime", //$NON-NLS-1$
                PREFS_THREAD_COL_STIME, store);

        TableHelper.createTableColumn(
                mThreadTable,
                "Name",
                SWT.LEFT,
                "android.class.ReallyLongClassName.MethodName", //$NON-NLS-1$
                PREFS_THREAD_COL_NAME, store);
        
        mThreadViewer = new TableViewer(mThreadTable);
        mThreadViewer.setContentProvider(new ThreadContentProvider());
        mThreadViewer.setLabelProvider(new ThreadLabelProvider());

        mThreadViewer.addSelectionChangedListener(new ISelectionChangedListener() {
            public void selectionChanged(SelectionChangedEvent event) {
                ThreadInfo selectedThread = getThreadSelection(event.getSelection());
                updateThreadStackTrace(selectedThread);
            }
        });
        mThreadViewer.addDoubleClickListener(new IDoubleClickListener() {
            public void doubleClick(DoubleClickEvent event) {
                ThreadInfo selectedThread = getThreadSelection(event.getSelection());
                if (selectedThread != null) {
                    Client client = (Client)mThreadViewer.getInput();
                    
                    if (client != null) {
                        client.requestThreadStackTrace(selectedThread.getThreadId());
                    }
                }
            }
        });
        
        // the separating sash
        final Sash sash = new Sash(mThreadBase, SWT.HORIZONTAL);
        Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
        sash.setBackground(darkGray);
        
        // the UI below the sash
        mStackTraceBase = new Composite(mThreadBase, SWT.NONE);
        mStackTraceBase.setLayout(new GridLayout(2, false));

        mRefreshStackTraceButton = new Button(mStackTraceBase, SWT.PUSH);
        mRefreshStackTraceButton.setText("Refresh");
        mRefreshStackTraceButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                ThreadInfo selectedThread = getThreadSelection(null);
                if (selectedThread != null) {
                    Client currentClient = getCurrentClient();
                    if (currentClient != null) {
                        currentClient.requestThreadStackTrace(selectedThread.getThreadId());
                    }
                }
            }
        });
        
        mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE);
        mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        mStackTracePanel = new StackTracePanel();
        mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase,
                PREFS_STACK_COL_CLASS,
                PREFS_STACK_COL_METHOD,
                PREFS_STACK_COL_FILE,
                PREFS_STACK_COL_LINE,
                PREFS_STACK_COL_NATIVE,
                store);
        
        GridData gd;
        mStackTraceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
        gd.horizontalSpan = 2;
        
        // now setup the sash.
        // form layout data
        FormData data = new FormData();
        data.top = new FormAttachment(0, 0);
        data.bottom = new FormAttachment(sash, 0);
        data.left = new FormAttachment(0, 0);
        data.right = new FormAttachment(100, 0);
        mThreadTable.setLayoutData(data);

        final FormData sashData = new FormData();
        if (store != null && store.contains(PREFS_THREAD_SASH)) {
            sashData.top = new FormAttachment(0, store.getInt(PREFS_THREAD_SASH));
        } else {
            sashData.top = new FormAttachment(50,0); // 50% across
        }
        sashData.left = new FormAttachment(0, 0);
        sashData.right = new FormAttachment(100, 0);
        sash.setLayoutData(sashData);

        data = new FormData();
        data.top = new FormAttachment(sash, 0);
        data.bottom = new FormAttachment(100, 0);
        data.left = new FormAttachment(0, 0);
        data.right = new FormAttachment(100, 0);
        mStackTraceBase.setLayoutData(data);

        // allow resizes, but cap at minPanelWidth
        sash.addListener(SWT.Selection, new Listener() {
            public void handleEvent(Event e) {
                Rectangle sashRect = sash.getBounds();
                Rectangle panelRect = mThreadBase.getClientArea();
                int bottom = panelRect.height - sashRect.height - 100;
                e.y = Math.max(Math.min(e.y, bottom), 100);
                if (e.y != sashRect.y) {
                    sashData.top = new FormAttachment(0, e.y);
                    store.setValue(PREFS_THREAD_SASH, e.y);
                    mThreadBase.layout();
                }
            }
        });

        ((StackLayout)mBase.getLayout()).topControl = mNotSelected;

        return mBase;
    }
    
    /**
     * Sets the focus to the proper control inside the panel.
     */
    @Override
    public void setFocus() {
        mThreadTable.setFocus();
    }

    /**
     * Sent when an existing client information changed.
     * <p/>
     * This is sent from a non UI thread.
     * @param client the updated client.
     * @param changeMask the bit mask describing the changed properties. It can contain
     * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
     * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
     * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
     * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
     *
     * @see IClientChangeListener#clientChanged(Client, int)
     */
    public void clientChanged(final Client client, int changeMask) {
        if (client == getCurrentClient()) {
            if ((changeMask & Client.CHANGE_THREAD_MODE) != 0 ||
                    (changeMask & Client.CHANGE_THREAD_DATA) != 0) {
                try {
                    mThreadTable.getDisplay().asyncExec(new Runnable() {
                        public void run() {
                            clientSelected();
                        }
                    });
                } catch (SWTException e) {
                    // widget is disposed, we do nothing
                }
            } else if ((changeMask & Client.CHANGE_THREAD_STACKTRACE) != 0) {
                try {
                    mThreadTable.getDisplay().asyncExec(new Runnable() {
                        public void run() {
                            updateThreadStackCall();
                        }
                    });
                } catch (SWTException e) {
                    // widget is disposed, we do nothing
                }
            }
        }
    }

    /**
     * Sent when a new device is selected. The new device can be accessed
     * with {@link #getCurrentDevice()}.
     */
    @Override
    public void deviceSelected() {
        // pass
    }

    /**
     * Sent when a new client is selected. The new client can be accessed
     * with {@link #getCurrentClient()}.
     */
    @Override
    public void clientSelected() {
        if (mThreadTable.isDisposed()) {
            return;
        }

        Client client = getCurrentClient();
        
        mStackTracePanel.setCurrentClient(client);

        if (client != null) {
            if (!client.isThreadUpdateEnabled()) {
                ((StackLayout)mBase.getLayout()).topControl = mNotEnabled;
                mThreadViewer.setInput(null);

                // if we are currently updating the thread, stop doing it.
                mMustStopRecurringThreadUpdate = true;
            } else {
                ((StackLayout)mBase.getLayout()).topControl = mThreadBase;
                mThreadViewer.setInput(client);

                synchronized (mLock) {
                    // if we're not updating we start the process
                    if (mRecurringThreadUpdateRunning == false) {
                        startRecurringThreadUpdate();
                    } else if (mMustStopRecurringThreadUpdate) {
                        // else if there's a runnable that's still going to get called, lets
                        // simply cancel the stop, and keep going
                        mMustStopRecurringThreadUpdate = false;
                    }
                }
            }
        } else {
            ((StackLayout)mBase.getLayout()).topControl = mNotSelected;
            mThreadViewer.setInput(null);
        }

        mBase.layout();
    }
    
    /**
     * Updates the stack call of the currently selected thread.
     * <p/>
     * This <b>must</b> be called from the UI thread.
     */
    private void updateThreadStackCall() {
        Client client = getCurrentClient();
        if (client != null) {
            // get the current selection in the ThreadTable
            ThreadInfo selectedThread = getThreadSelection(null);
            
            if (selectedThread != null) {
                updateThreadStackTrace(selectedThread);
            } else {
                updateThreadStackTrace(null);
            }
        }
    }
    
    /**
     * updates the stackcall of the specified thread. If <code>null</code> the UI is emptied
     * of current data.
     * @param thread
     */
    private void updateThreadStackTrace(ThreadInfo thread) {
        mStackTracePanel.setViewerInput(thread);
        
        if (thread != null) {
            mRefreshStackTraceButton.setEnabled(true);
            long stackcallTime = thread.getStackCallTime();
            if (stackcallTime != 0) {
                String label = new Date(stackcallTime).toString();
                mStackTraceTimeLabel.setText(label);
            } else {
                mStackTraceTimeLabel.setText(""); //$NON-NLS-1$
            }
        } else {
            mRefreshStackTraceButton.setEnabled(true);
            mStackTraceTimeLabel.setText(""); //$NON-NLS-1$
        }
    }

    @Override
    protected void setTableFocusListener() {
        addTableToFocusListener(mThreadTable);
        addTableToFocusListener(mStackTraceTable);
    }

    /**
     * Initiate recurring events. We use a shorter "initialWait" so we do the
     * first execution sooner. We don't do it immediately because we want to
     * give the clients a chance to get set up.
     */
    private void startRecurringThreadUpdate() {
        mRecurringThreadUpdateRunning = true;
        int initialWait = 1000;

        mDisplay.timerExec(initialWait, new Runnable() {
            public void run() {
                synchronized (mLock) {
                    // lets check we still want updates.
                    if (mMustStopRecurringThreadUpdate == false) {
                        Client client = getCurrentClient();
                        if (client != null) {
                            client.requestThreadUpdate();

                            mDisplay.timerExec(
                                    DdmUiPreferences.getThreadRefreshInterval() * 1000, this);
                        } else {
                            // we don't have a Client, which means the runnable is not
                            // going to be called through the timer. We reset the running flag.
                            mRecurringThreadUpdateRunning = false;
                        }
                    } else {
                        // else actually stops (don't call the timerExec) and reset the flags.
                        mRecurringThreadUpdateRunning = false;
                        mMustStopRecurringThreadUpdate = false;
                    }
                }
            }
        });
    }
    
    /**
     * Returns the current thread selection or <code>null</code> if none is found.
     * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection
     * is returned, otherwise, the <code>ISelection</code> returned by
     * {@link TableViewer#getSelection()} is used.
     * @param selection the {@link ISelection} to use, or <code>null</code>
     */
    private ThreadInfo getThreadSelection(ISelection selection) {
        if (selection == null) {
            selection = mThreadViewer.getSelection();
        }
        
        if (selection instanceof IStructuredSelection) {
            IStructuredSelection structuredSelection = (IStructuredSelection)selection;
            Object object = structuredSelection.getFirstElement();
            if (object instanceof ThreadInfo) {
                return (ThreadInfo)object;
            }
        }
        
        return null;
    }

}

