/*******************************************************************************
 * Copyright (c) 2010 JVM Monitor project. All rights reserved. 
 * 
 * This code is distributed under the terms of the Eclipse Public License v1.0
 * which is available at http://www.eclipse.org/legal/epl-v10.html
 *******************************************************************************/
package org.jvmmonitor.internal.ui.properties.timeline;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Cursor;
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.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.forms.events.HyperlinkAdapter;
import org.eclipse.ui.forms.events.HyperlinkEvent;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.Hyperlink;
import org.eclipse.ui.part.PageBook;
import org.jvmmonitor.core.IActiveJvm;
import org.jvmmonitor.core.JvmCoreException;
import org.jvmmonitor.core.JvmModelEvent;
import org.jvmmonitor.core.JvmModelEvent.State;
import org.jvmmonitor.core.mbean.IMBeanServer;
import org.jvmmonitor.core.mbean.IMBeanServerChangeListener;
import org.jvmmonitor.core.mbean.IMonitoredMXBeanAttribute;
import org.jvmmonitor.core.mbean.IMonitoredMXBeanGroup;
import org.jvmmonitor.core.mbean.MBeanServerEvent;
import org.jvmmonitor.internal.ui.IHelpContextIds;
import org.jvmmonitor.internal.ui.RefreshJob;
import org.jvmmonitor.internal.ui.actions.RefreshAction;
import org.jvmmonitor.internal.ui.properties.AbstractJvmPropertySection;
import org.jvmmonitor.internal.ui.properties.memory.GarbageCollectorAction;
import org.jvmmonitor.ui.Activator;

/**
 * The timeline section.
 */
public class TimelineSection extends AbstractJvmPropertySection {

    /** The charts. */
    private List<TimelineChart> charts;

    /** The action to clear timeline data. */
    private ClearTimelineDataAction clearAction;

    /** The action to refresh section. */
    private RefreshAction refreshAction;

    /** The action to create a new chart. */
    private NewChartAction newChartAction;

    /** The action to load chart set. */
    private LoadChartSetAction loadChartSetAction;

    /** The action to run garbage collector. */
    private GarbageCollectorAction garbageCollectorAction;

    /** The action to save chart set as given name. */
    private SaveChartSetAsAction saveChartSetAsAction;

    /** The MBean server change listener. */
    private IMBeanServerChangeListener mBeanServerChangeListener;

    /** The charts page. */
    private Composite chartsPage;

    /** The message page. */
    private Composite messagePage;

    /** The page book. */
    private PageBook timelinePageBook;

    /**
     * The constructor.
     */
    public TimelineSection() {
        charts = new ArrayList<TimelineChart>();
        clearAction = new ClearTimelineDataAction(this);
        refreshAction = new RefreshAction(this);
        newChartAction = new NewChartAction(this);
        loadChartSetAction = new LoadChartSetAction(this);
        garbageCollectorAction = new GarbageCollectorAction(this);
        saveChartSetAsAction = new SaveChartSetAsAction(this);
    }

    /*
     * @see AbstractJvmPropertySection#createControls(Composite)
     */
    @Override
    protected void createControls(Composite parent) {
        timelinePageBook = new PageBook(parent, SWT.NONE);
        chartsPage = new Composite(timelinePageBook, SWT.NONE);
        messagePage = createMessagePage();

        parent.setBackground(Display.getDefault().getSystemColor(
                SWT.COLOR_LIST_BACKGROUND));
        GridLayout layout = new GridLayout(2, true);
        layout.horizontalSpacing = 15;
        chartsPage.setLayout(layout);
        timelinePageBook.showPage(chartsPage);

        mBeanServerChangeListener = new IMBeanServerChangeListener() {
            @Override
            public void serverChanged(MBeanServerEvent event) {
                new RefreshJob(Messages.reconstructChartJobLabel, toString()) {
                    @Override
                    protected void refreshUI() {
                        IActiveJvm jvm = getJvm();
                        if (jvm != null) {
                            reconstructCharts(jvm, false);
                        }
                    }
                }.schedule();
            }
        };

        PlatformUI.getWorkbench().getHelpSystem()
                .setHelp(parent, IHelpContextIds.TIMELINE_PAGE);
    }

    /*
     * @see AbstractJvmPropertySection#jvmModelChanged(JvmModelEvent)
     */
    @Override
    public void jvmModelChanged(JvmModelEvent event) {
        super.jvmModelChanged(event);

        if (event.state == State.JvmConnected && !chartsPage.isDisposed()) {
            final IActiveJvm newJvm = (IActiveJvm) event.jvm;
            Display.getDefault().asyncExec(new Runnable() {
                @Override
                public void run() {
                    reconstructCharts(newJvm, true);
                }
            });
            newJvm.getMBeanServer().addServerChangeListener(
                    mBeanServerChangeListener);
        }
    }

    /*
     * @see AbstractJvmPropertySection#setInput(IWorkbenchPart, ISelection,
     * IActiveJvm, IActiveJvm)
     */
    @Override
    protected void setInput(IWorkbenchPart part, ISelection selection,
            IActiveJvm newJvm, IActiveJvm oldJvm) {
        if (newJvm == null || newJvm.equals(oldJvm)) {
            return;
        }

        reconstructCharts(newJvm, false);

        if (oldJvm != null && mBeanServerChangeListener != null) {
            IMBeanServer server = oldJvm.getMBeanServer();
            if (server != null) {
                server.removeServerChangeListener(mBeanServerChangeListener);
            }
        }

        newJvm.getMBeanServer().addServerChangeListener(
                mBeanServerChangeListener);
    }

    /*
     * @see AbstractPropertySection#refresh()
     */
    @Override
    public void refresh() {
        refreshConnectionIndicator();

        IActiveJvm jvm = getJvm();
        if (jvm == null || !jvm.isConnected() || isRefreshSuspended()
                || chartsPage.isDisposed()) {
            return;
        }

        for (TimelineChart chart : charts) {
            if (!chart.isDisposed() && chart.isVisible()) {
                chart.refresh();
            }
        }
    }
    
    /*
     * @see AbstractJvmPropertySection#addToolBarActions(IToolBarManager)
     */
    @Override
    protected void addToolBarActions(IToolBarManager manager) {
        if (manager.find("separator") == null) { //$NON-NLS-1$
            manager.insertAfter("defaults", new Separator("separator")); //$NON-NLS-1$ //$NON-NLS-2$
        }
        if (manager.find(refreshAction.getId()) == null) {
            manager.insertAfter("defaults", refreshAction); //$NON-NLS-1$
        }
        if (manager.find(clearAction.getId()) == null) {
            manager.insertAfter("defaults", clearAction); //$NON-NLS-1$
        }
        if (manager.find(newChartAction.getId()) == null) {
            manager.insertAfter("defaults", newChartAction); //$NON-NLS-1$
        }
    }

    /*
     * @see AbstractJvmPropertySection#removeToolBarActions(IToolBarManager)
     */
    @Override
    protected void removeToolBarActions(IToolBarManager manager) {
        manager.remove("separator"); //$NON-NLS-1$
        manager.remove(refreshAction.getId());
        manager.remove(clearAction.getId());
        manager.remove(newChartAction.getId());
    }

    /*
     * @see AbstractJvmPropertySection#addLocalMenus(IMenuManager)
     */
    @Override
    protected void addLocalMenus(IMenuManager manager) {
        if (manager.find(saveChartSetAsAction.getId()) == null) {
            manager.add(saveChartSetAsAction);
        }
        if (manager.find(loadChartSetAction.getId()) == null) {
            manager.add(loadChartSetAction);
        }
        if (manager.find("separator") == null) { //$NON-NLS-1$
            manager.add(new Separator("separator")); //$NON-NLS-1$
        }
    }

    /*
     * @see AbstractJvmPropertySection#removeLocalMenus(IMenuManager)
     */
    @Override
    protected void removeLocalMenus(IMenuManager manager) {
        manager.remove(saveChartSetAsAction.getId());
        manager.remove(loadChartSetAction.getId());
        manager.remove("separator"); //$NON-NLS-1$
    }

    /*
     * @see AbstractJvmPropertySection#dispose()
     */
    @Override
    public void dispose() {
        super.dispose();
        IActiveJvm jvm = getJvm();
        if (jvm != null) {
            IMBeanServer server = jvm.getMBeanServer();
            if (server != null) {
                server.removeServerChangeListener(mBeanServerChangeListener);
            }
        }
    }

    /**
     * Clears the monitored attributes.
     */
    protected void clear() {
        final IActiveJvm jvm = getJvm();

        if (jvm != null) {
            for (IMonitoredMXBeanGroup group : jvm.getMBeanServer()
                    .getMonitoredAttributeGroups()) {
                group.clearAttributes();
            }

            if (isRefreshSuspended()) {
                Display.getDefault().asyncExec(new Runnable() {
                    @Override
                    public void run() {
                        reconstructCharts(jvm, false);
                    }
                });
            }
        }
    }

    /**
     * Creates the message page.
     * 
     * @return The message page
     */
    private Composite createMessagePage() {
        messagePage = new Composite(timelinePageBook, SWT.NONE);
        messagePage.setLayout(new GridLayout(3, false));
        messagePage.setBackground(Display.getDefault().getSystemColor(
                SWT.COLOR_LIST_BACKGROUND));

        FormToolkit toolkit = new FormToolkit(Display.getDefault());
        Hyperlink hyperlink = toolkit.createHyperlink(messagePage,
                Messages.loadDefaultChartSetLabel, SWT.NONE);
        hyperlink.addHyperlinkListener(new HyperlinkAdapter() {
            @Override
            public void linkActivated(HyperlinkEvent event) {
                try {
                    new LoadChartSetAction(TimelineSection.this)
                            .loadDefaultChartSet();
                } catch (JvmCoreException e) {
                    Activator.log(Messages.loadChartSetFailedMsg, e);
                }
            }
        });
        return messagePage;
    }

    /**
     * Refreshes the connection indicator.
     */
    private void refreshConnectionIndicator() {
        IActiveJvm jvm = getJvm();
        boolean isConnected = jvm != null && jvm.isConnected();
        refreshBackground(chartsPage, isConnected);
        for (TimelineChart chart : charts) {
            refreshBackground(chart, isConnected);
            refreshBackground(chart.getPlotArea(), isConnected);
            refreshBackground(chart.getSection(), isConnected);
        }
        refreshBackground(messagePage, isConnected);

        clearAction.setEnabled(isConnected);
        refreshAction.setEnabled(isConnected);
        newChartAction.setEnabled(isConnected);
        loadChartSetAction.setEnabled(isConnected);
        garbageCollectorAction.setEnabled(isConnected);
    }

    /**
     * Reconstructs charts.
     * <p>
     * When JVM gets connected (e.g. opening Properties view or on already
     * opened Properties view), the default chart set will be loaded and
     * applied.
     * <p>
     * When target JVM is already connected (e.g. opening new Properties view,
     * another JVM gets selected, or monitored attributes gets changed), chart
     * configuration will be adapted to the monitored attributes stored in the
     * target JVM.
     * 
     * @param activeJvm
     *            The JVM
     * @param connected
     *            True if JVM gets connected
     */
    void reconstructCharts(IActiveJvm activeJvm, boolean connected) {
        if (chartsPage.isDisposed()) {
            return;
        }

        if (connected) {
            try {
                loadChartSetAction.loadDefaultChartSet();
            } catch (JvmCoreException e) {
                Activator
                        .log(Messages.configureMonitoredAttributesFailedMsg, e);
            }
        }

        List<IMonitoredMXBeanGroup> groups = activeJvm.getMBeanServer()
                .getMonitoredAttributeGroups();
        if (groups.size() == 0) {
            timelinePageBook.showPage(messagePage);
            return;
        }

        timelinePageBook.showPage(chartsPage);
        chartsPage.setVisible(false);
        for (TimelineChart chart : charts) {
            chart.dispose();
        }
        charts.clear();

        GridLayout layout = (GridLayout) chartsPage.getLayout();
        layout.numColumns = (groups.size() > 1) ? 2 : 1;
        chartsPage.setLayout(layout);

        for (IMonitoredMXBeanGroup group : groups) {
            createSection(chartsPage, group);
        }

        chartsPage.layout();
        chartsPage.setVisible(true);
        refresh();
    }

    /**
     * Creates the section.
     * 
     * @param parent
     *            The parent composite
     * @param group
     *            The attribute group
     */
    private void createSection(Composite parent, IMonitoredMXBeanGroup group) {
        if (parent.isDisposed()) {
            return;
        }

        ExpandableComposite section = getWidgetFactory().createSection(parent,
                ExpandableComposite.TITLE_BAR);
        section.setText(group.getName());
        section.setLayoutData(new GridData(GridData.FILL_BOTH));

        Composite flatFormComposite = getWidgetFactory()
                .createFlatFormComposite(section);
        FormLayout formLayout = new FormLayout();
        formLayout.marginHeight = 0;
        formLayout.marginWidth = 0;
        flatFormComposite.setLayout(formLayout);
        TimelineChart chart = new TimelineChart(flatFormComposite, section,
                group, SWT.NONE, toString() + getJvm().getPid());
        charts.add(chart);

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

        section.setClient(flatFormComposite);
        List<Action> actions = new ArrayList<Action>();
        for (IMonitoredMXBeanAttribute attribute : group.getAttributes()) {
            if (attribute.getAttributeName().startsWith("HeapMemoryUsage")) { //$NON-NLS-1$
                actions.add(garbageCollectorAction);
                break;
            }
        }
        actions.add(new ConfigureChartAction(chart, this));
        actions.add(new DeleteChartAction(chart, this));
        addSectionActions(section, actions);
    }

    /**
     * Adds the menus on expandable composite.
     * 
     * @param expandableComposite
     *            The expandable composite
     * @param actions
     *            The actions
     */
    private static void addSectionActions(
            ExpandableComposite expandableComposite, List<Action> actions) {
        ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
        ToolBar toolbar = toolBarManager.createControl(expandableComposite);

        // set cursor
        final Cursor cursor = new Cursor(Display.getCurrent(), SWT.CURSOR_HAND);
        toolbar.setCursor(cursor);
        toolbar.addDisposeListener(new DisposeListener() {
            @Override
            public void widgetDisposed(DisposeEvent e) {
                if (!cursor.isDisposed()) {
                    cursor.dispose();
                }
            }
        });

        // add menus
        for (Action action : actions) {
            toolBarManager.add(action);
        }
        toolBarManager.update(true);

        expandableComposite.setTextClient(toolbar);
    }
}
