/*
 * Copyright (C) 2006 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.traceview;

import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeViewerListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeExpansionEvent;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
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.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;

import java.util.ArrayList;
import java.util.Observable;
import java.util.Observer;

public class ProfileView extends Composite implements Observer {

    private TreeViewer mTreeViewer;
    private Text mSearchBox;
    private SelectionController mSelectionController;
    private ProfileProvider mProfileProvider;
    private Color mColorNoMatch;
    private Color mColorMatch;
    private MethodData mCurrentHighlightedMethod;
    private MethodHandler mMethodHandler;

    public interface MethodHandler {
        void handleMethod(MethodData method);
    }

    public ProfileView(Composite parent, TraceReader reader,
            SelectionController selectController) {
        super(parent, SWT.NONE);
        setLayout(new GridLayout(1, false));
        this.mSelectionController = selectController;
        mSelectionController.addObserver(this);

        // Add a tree viewer at the top
        mTreeViewer = new TreeViewer(this, SWT.MULTI | SWT.NONE);
        mTreeViewer.setUseHashlookup(true);
        mProfileProvider = reader.getProfileProvider();
        mProfileProvider.setTreeViewer(mTreeViewer);
        SelectionAdapter listener = mProfileProvider.getColumnListener();
        final Tree tree = mTreeViewer.getTree();
        tree.setHeaderVisible(true);
        tree.setLayoutData(new GridData(GridData.FILL_BOTH));

        // Get the column names from the ProfileProvider
        String[] columnNames = mProfileProvider.getColumnNames();
        int[] columnWidths = mProfileProvider.getColumnWidths();
        int[] columnAlignments = mProfileProvider.getColumnAlignments();
        for (int ii = 0; ii < columnWidths.length; ++ii) {
            TreeColumn column = new TreeColumn(tree, SWT.LEFT);
            column.setText(columnNames[ii]);
            column.setWidth(columnWidths[ii]);
            column.setMoveable(true);
            column.addSelectionListener(listener);
            column.setAlignment(columnAlignments[ii]);
        }

        // Add a listener to the tree so that we can make the row
        // height smaller.
        tree.addListener(SWT.MeasureItem, new Listener() {
            @Override
            public void handleEvent(Event event) {
                int fontHeight = event.gc.getFontMetrics().getHeight();
                event.height = fontHeight;
            }
        });

        mTreeViewer.setContentProvider(mProfileProvider);
        mTreeViewer.setLabelProvider(mProfileProvider.getLabelProvider());
        mTreeViewer.setInput(mProfileProvider.getRoot());

        // Create another composite to hold the label and text box
        Composite composite = new Composite(this, SWT.NONE);
        composite.setLayout(new GridLayout(2, false));
        composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        // Add a label for the search box
        Label label = new Label(composite, SWT.NONE);
        label.setText("Find:");

        // Add a text box for searching for method names
        mSearchBox = new Text(composite, SWT.BORDER);
        mSearchBox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        Display display = getDisplay();
        mColorNoMatch = new Color(display, 255, 200, 200);
        mColorMatch = mSearchBox.getBackground();

        mSearchBox.addModifyListener(new ModifyListener() {
            @Override
            public void modifyText(ModifyEvent ev) {
                String query = mSearchBox.getText();
                if (query.length() == 0)
                    return;
                findName(query);
            }
        });

        // Add a key listener to the text box so that we can clear
        // the text box if the user presses <ESC>.
        mSearchBox.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent event) {
                if (event.keyCode == SWT.ESC) {
                    mSearchBox.setText("");
                } else if (event.keyCode == SWT.CR) {
                    String query = mSearchBox.getText();
                    if (query.length() == 0)
                        return;
                    findNextName(query);
                }
            }
        });

        // Also add a key listener to the tree viewer so that the
        // user can just start typing anywhere in the tree view.
        tree.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent event) {
                if (event.keyCode == SWT.ESC) {
                    mSearchBox.setText("");
                } else if (event.keyCode == SWT.BS) {
                    // Erase the last character from the search box
                    String text = mSearchBox.getText();
                    int len = text.length();
                    String chopped;
                    if (len <= 1)
                        chopped = "";
                    else
                        chopped = text.substring(0, len - 1);
                    mSearchBox.setText(chopped);
                } else if (event.keyCode == SWT.CR) {
                    String query = mSearchBox.getText();
                    if (query.length() == 0)
                        return;
                    findNextName(query);
                } else {
                    // Append the given character to the search box
                    String str = String.valueOf(event.character);
                    mSearchBox.append(str);
                }
                event.doit = false;
            }
        });

        // Add a selection listener to the tree so that the user can click
        // on a method that is a child or parent and jump to that method.
        mTreeViewer
                .addSelectionChangedListener(new ISelectionChangedListener() {
                    @Override
                    public void selectionChanged(SelectionChangedEvent ev) {
                        ISelection sel = ev.getSelection();
                        if (sel.isEmpty())
                            return;
                        if (sel instanceof IStructuredSelection) {
                            IStructuredSelection selection = (IStructuredSelection) sel;
                            Object element = selection.getFirstElement();
                            if (element == null)
                                return;
                            if (element instanceof MethodData) {
                                MethodData md = (MethodData) element;
                                highlightMethod(md, true);
                            }
                            if (element instanceof ProfileData) {
                                MethodData md = ((ProfileData) element)
                                        .getMethodData();
                                highlightMethod(md, true);
                            }
                        }
                    }
                });

        // Add a tree listener so that we can expand the parents and children
        // of a method when a method is expanded.
        mTreeViewer.addTreeListener(new ITreeViewerListener() {
            @Override
            public void treeExpanded(TreeExpansionEvent event) {
                Object element = event.getElement();
                if (element instanceof MethodData) {
                    MethodData md = (MethodData) element;
                    expandNode(md);
                }
            }
            @Override
            public void treeCollapsed(TreeExpansionEvent event) {
            }
        });

        tree.addListener(SWT.MouseDown, new Listener() {
            @Override
            public void handleEvent(Event event) {
                Point point = new Point(event.x, event.y);
                TreeItem treeItem = tree.getItem(point);
                MethodData md = mProfileProvider.findMatchingTreeItem(treeItem);
                if (md == null)
                    return;
                ArrayList<Selection> selections = new ArrayList<Selection>();
                selections.add(Selection.highlight("MethodData", md));
                mSelectionController.change(selections, "ProfileView");

                if (mMethodHandler != null && (event.stateMask & SWT.MOD1) != 0) {
                    mMethodHandler.handleMethod(md);
                }
            }
        });
    }

    public void setMethodHandler(MethodHandler handler) {
        mMethodHandler = handler;
    }

    private void findName(String query) {
        MethodData md = mProfileProvider.findMatchingName(query);
        selectMethod(md);
    }

    private void findNextName(String query) {
        MethodData md = mProfileProvider.findNextMatchingName(query);
        selectMethod(md);
    }

    private void selectMethod(MethodData md) {
        if (md == null) {
            mSearchBox.setBackground(mColorNoMatch);
            return;
        }
        mSearchBox.setBackground(mColorMatch);
        highlightMethod(md, false);
    }

    @Override
    public void update(Observable objservable, Object arg) {
        // Ignore updates from myself
        if (arg == "ProfileView")
            return;
        // System.out.printf("profileview update from %s\n", arg);
        ArrayList<Selection> selections;
        selections = mSelectionController.getSelections();
        for (Selection selection : selections) {
            Selection.Action action = selection.getAction();
            if (action != Selection.Action.Highlight)
                continue;
            String name = selection.getName();
            if (name == "MethodData") {
                MethodData md = (MethodData) selection.getValue();
                highlightMethod(md, true);
                return;
            }
            if (name == "Call") {
                Call call = (Call) selection.getValue();
                MethodData md = call.getMethodData();
                highlightMethod(md, true);
                return;
            }
        }
    }

    private void highlightMethod(MethodData md, boolean clearSearch) {
        if (md == null)
            return;
        // Avoid an infinite recursion
        if (md == mCurrentHighlightedMethod)
            return;
        if (clearSearch) {
            mSearchBox.setText("");
            mSearchBox.setBackground(mColorMatch);
        }
        mCurrentHighlightedMethod = md;
        mTreeViewer.collapseAll();
        // Expand this node and its children
        expandNode(md);
        StructuredSelection sel = new StructuredSelection(md);
        mTreeViewer.setSelection(sel, true);
        Tree tree = mTreeViewer.getTree();
        TreeItem[] items = tree.getSelection();
        if (items.length != 0) {
            tree.setTopItem(items[0]);
            // workaround a Mac bug by adding showItem().
            tree.showItem(items[0]);
        }
    }

    private void expandNode(MethodData md) {
        ProfileNode[] nodes = md.getProfileNodes();
        mTreeViewer.setExpandedState(md, true);
        // Also expand the "Parents" and "Children" nodes.
        if (nodes != null) {
            for (ProfileNode node : nodes) {
                if (node.isRecursive() == false)
                    mTreeViewer.setExpandedState(node, true);
            }
        }
    }
}
