/*
 * Copyright (c) 2004, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.tools.jconsole;

import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.List;
import java.util.Timer;
import javax.swing.*;
import javax.swing.plaf.*;


import com.sun.tools.jconsole.JConsolePlugin;
import com.sun.tools.jconsole.JConsoleContext;

import static sun.tools.jconsole.ProxyClient.*;

@SuppressWarnings("serial")
public class VMPanel extends JTabbedPane implements PropertyChangeListener {

    private ProxyClient proxyClient;
    private Timer timer;
    private int updateInterval;
    private String hostName;
    private int port;
    private String userName;
    private String password;
    private String url;
    private VMInternalFrame vmIF = null;
    private static ArrayList<TabInfo> tabInfos = new ArrayList<TabInfo>();
    private boolean wasConnected = false;
    private boolean userDisconnected = false;
    private boolean shouldUseSSL = true;

    // The everConnected flag keeps track of whether the window can be
    // closed if the user clicks Cancel after a failed connection attempt.
    //
    private boolean everConnected = false;

    // The initialUpdate flag is used to enable/disable tabs each time
    // a connect or reconnect takes place. This flag avoids having to
    // enable/disable tabs on each update call.
    //
    private boolean initialUpdate = true;

    // Each VMPanel has its own instance of the JConsolePlugin
    // A map of JConsolePlugin to the previous SwingWorker
    private Map<ExceptionSafePlugin, SwingWorker<?, ?>> plugins = null;
    private boolean pluginTabsAdded = false;

    // Update these only on the EDT
    private JOptionPane optionPane;
    private JProgressBar progressBar;
    private long time0;

    static {
        tabInfos.add(new TabInfo(OverviewTab.class, OverviewTab.getTabName(), true));
        tabInfos.add(new TabInfo(MemoryTab.class, MemoryTab.getTabName(), true));
        tabInfos.add(new TabInfo(ThreadTab.class, ThreadTab.getTabName(), true));
        tabInfos.add(new TabInfo(ClassTab.class, ClassTab.getTabName(), true));
        tabInfos.add(new TabInfo(SummaryTab.class, SummaryTab.getTabName(), true));
        tabInfos.add(new TabInfo(MBeansTab.class, MBeansTab.getTabName(), true));
    }

    public static TabInfo[] getTabInfos() {
        return tabInfos.toArray(new TabInfo[tabInfos.size()]);
    }

    VMPanel(ProxyClient proxyClient, int updateInterval) {
        this.proxyClient = proxyClient;
        this.updateInterval = updateInterval;
        this.hostName = proxyClient.getHostName();
        this.port = proxyClient.getPort();
        this.userName = proxyClient.getUserName();
        this.password = proxyClient.getPassword();
        this.url = proxyClient.getUrl();

        for (TabInfo tabInfo : tabInfos) {
            if (tabInfo.tabVisible) {
                addTab(tabInfo);
            }
        }

        plugins = new LinkedHashMap<ExceptionSafePlugin, SwingWorker<?, ?>>();
        for (JConsolePlugin p : JConsole.getPlugins()) {
            p.setContext(proxyClient);
            plugins.put(new ExceptionSafePlugin(p), null);
        }

        Utilities.updateTransparency(this);

        ToolTipManager.sharedInstance().registerComponent(this);

        // Start listening to connection state events
        //
        proxyClient.addPropertyChangeListener(this);

        addMouseListener(new MouseAdapter() {

            public void mouseClicked(MouseEvent e) {
                if (connectedIconBounds != null && (e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0 && connectedIconBounds.contains(e.getPoint())) {

                    if (isConnected()) {
                        userDisconnected = true;
                        disconnect();
                        wasConnected = false;
                    } else {
                        connect();
                    }
                    repaint();
                }
            }
        });

    }
    private static Icon connectedIcon16 =
            new ImageIcon(VMPanel.class.getResource("resources/connected16.png"));
    private static Icon connectedIcon24 =
            new ImageIcon(VMPanel.class.getResource("resources/connected24.png"));
    private static Icon disconnectedIcon16 =
            new ImageIcon(VMPanel.class.getResource("resources/disconnected16.png"));
    private static Icon disconnectedIcon24 =
            new ImageIcon(VMPanel.class.getResource("resources/disconnected24.png"));
    private Rectangle connectedIconBounds;

    // Override to increase right inset for tab area,
    // in order to reserve space for the connect toggle.
    public void setUI(TabbedPaneUI ui) {
        Insets insets = (Insets) UIManager.getLookAndFeelDefaults().get("TabbedPane.tabAreaInsets");
        if (insets != null) {
            insets = (Insets) insets.clone();
            insets.right += connectedIcon24.getIconWidth() + 8;
            UIManager.put("TabbedPane.tabAreaInsets", insets);
        }
        super.setUI(ui);
    }

    // Override to paint the connect toggle
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        Icon icon;
        Component c0 = getComponent(0);
        if (c0 != null && c0.getY() > 24) {
            icon = isConnected() ? connectedIcon24 : disconnectedIcon24;
        } else {
            icon = isConnected() ? connectedIcon16 : disconnectedIcon16;
        }
        Insets insets = getInsets();
        int x = getWidth() - insets.right - icon.getIconWidth() - 4;
        int y = insets.top;
        if (c0 != null) {
            y = (c0.getY() - icon.getIconHeight()) / 2;
        }
        icon.paintIcon(this, g, x, y);
        connectedIconBounds = new Rectangle(x, y, icon.getIconWidth(), icon.getIconHeight());
    }

    public String getToolTipText(MouseEvent event) {
        if (connectedIconBounds.contains(event.getPoint())) {
            if (isConnected()) {
                return Messages.CONNECTED_PUNCTUATION_CLICK_TO_DISCONNECT_;
            } else {
                return Messages.DISCONNECTED_PUNCTUATION_CLICK_TO_CONNECT_;
            }
        } else {
            return super.getToolTipText(event);
        }
    }

    private synchronized void addTab(TabInfo tabInfo) {
        Tab tab = instantiate(tabInfo);
        if (tab != null) {
            addTab(tabInfo.name, tab);
        } else {
            tabInfo.tabVisible = false;
        }
    }

    private synchronized void insertTab(TabInfo tabInfo, int index) {
        Tab tab = instantiate(tabInfo);
        if (tab != null) {
            insertTab(tabInfo.name, null, tab, null, index);
        } else {
            tabInfo.tabVisible = false;
        }
    }

    public synchronized void removeTabAt(int index) {
        super.removeTabAt(index);
    }

    private Tab instantiate(TabInfo tabInfo) {
        try {
            Constructor<?> con = tabInfo.tabClass.getConstructor(VMPanel.class);
            return (Tab) con.newInstance(this);
        } catch (Exception ex) {
            System.err.println(ex);
            return null;
        }
    }

    boolean isConnected() {
        return proxyClient.isConnected();
    }

    public int getUpdateInterval() {
        return updateInterval;
    }

    /**
     * WARNING NEVER CALL THIS METHOD TO MAKE JMX REQUEST
     * IF  assertThread == false.
     * DISPATCHER THREAD IS NOT ASSERTED.
     * IT IS USED TO MAKE SOME LOCAL MANIPULATIONS.
     */
    ProxyClient getProxyClient(boolean assertThread) {
        if (assertThread) {
            return getProxyClient();
        } else {
            return proxyClient;
        }
    }

    public ProxyClient getProxyClient() {
        String threadClass = Thread.currentThread().getClass().getName();
        if (threadClass.equals("java.awt.EventDispatchThread")) {
            String msg = "Calling VMPanel.getProxyClient() from the Event Dispatch Thread!";
            new RuntimeException(msg).printStackTrace();
            System.exit(1);
        }
        return proxyClient;
    }

    public void cleanUp() {
        //proxyClient.disconnect();
        for (Tab tab : getTabs()) {
            tab.dispose();
        }
        for (JConsolePlugin p : plugins.keySet()) {
            p.dispose();
        }
        // Cancel pending update tasks
        //
        if (timer != null) {
            timer.cancel();
        }
        // Stop listening to connection state events
        //
        proxyClient.removePropertyChangeListener(this);
    }

    // Call on EDT
    public void connect() {
        if (isConnected()) {
            // create plugin tabs if not done
            createPluginTabs();
            // Notify tabs
            fireConnectedChange(true);
            // Enable/disable tabs on initial update
            initialUpdate = true;
            // Start/Restart update timer on connect/reconnect
            startUpdateTimer();
        } else {
            new Thread("VMPanel.connect") {

                public void run() {
                    proxyClient.connect(shouldUseSSL);
                }
            }.start();
        }
    }

    // Call on EDT
    public void disconnect() {
        proxyClient.disconnect();
        updateFrameTitle();
    }

    // Called on EDT
    public void propertyChange(PropertyChangeEvent ev) {
        String prop = ev.getPropertyName();

        if (prop == CONNECTION_STATE_PROPERTY) {
            ConnectionState oldState = (ConnectionState) ev.getOldValue();
            ConnectionState newState = (ConnectionState) ev.getNewValue();
            switch (newState) {
                case CONNECTING:
                    onConnecting();
                    break;

                case CONNECTED:
                    if (progressBar != null) {
                        progressBar.setIndeterminate(false);
                        progressBar.setValue(100);
                    }
                    closeOptionPane();
                    updateFrameTitle();
                    // create tabs if not done
                    createPluginTabs();
                    repaint();
                    // Notify tabs
                    fireConnectedChange(true);
                    // Enable/disable tabs on initial update
                    initialUpdate = true;
                    // Start/Restart update timer on connect/reconnect
                    startUpdateTimer();
                    break;

                case DISCONNECTED:
                    if (progressBar != null) {
                        progressBar.setIndeterminate(false);
                        progressBar.setValue(0);
                        closeOptionPane();
                    }
                    vmPanelDied();
                    if (oldState == ConnectionState.CONNECTED) {
                        // Notify tabs
                        fireConnectedChange(false);
                    }
                    break;
            }
        }
    }

    // Called on EDT
    private void onConnecting() {
        time0 = System.currentTimeMillis();

        SwingUtilities.getWindowAncestor(this);

        String connectionName = getConnectionName();
        progressBar = new JProgressBar();
        progressBar.setIndeterminate(true);
        JPanel progressPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
        progressPanel.add(progressBar);

        Object[] message = {
            "<html><h3>" + Resources.format(Messages.CONNECTING_TO1, connectionName) + "</h3></html>",
            progressPanel,
            "<html><b>" + Resources.format(Messages.CONNECTING_TO2, connectionName) + "</b></html>"
        };

        optionPane =
                SheetDialog.showOptionDialog(this,
                message,
                JOptionPane.DEFAULT_OPTION,
                JOptionPane.INFORMATION_MESSAGE, null,
                new String[]{Messages.CANCEL},
                0);


    }

    // Called on EDT
    private void closeOptionPane() {
        if (optionPane != null) {
            new Thread("VMPanel.sleeper") {
                public void run() {
                    long elapsed = System.currentTimeMillis() - time0;
                    if (elapsed < 2000) {
                        try {
                            sleep(2000 - elapsed);
                        } catch (InterruptedException ex) {
                        // Ignore
                        }
                    }
                    SwingUtilities.invokeLater(new Runnable() {

                        public void run() {
                            optionPane.setVisible(false);
                            progressBar = null;
                        }
                    });
                }
            }.start();
        }
    }

    void updateFrameTitle() {
        VMInternalFrame vmIF = getFrame();
        if (vmIF != null) {
            String displayName = getDisplayName();
            if (!proxyClient.isConnected()) {
                displayName = Resources.format(Messages.CONNECTION_NAME__DISCONNECTED_, displayName);
            }
            vmIF.setTitle(displayName);
        }
    }

    private VMInternalFrame getFrame() {
        if (vmIF == null) {
            vmIF = (VMInternalFrame) SwingUtilities.getAncestorOfClass(VMInternalFrame.class,
                    this);
        }
        return vmIF;
    }

    // TODO: this method is not needed when all JConsole tabs
    // are migrated to use the new JConsolePlugin API.
    //
    // A thread safe clone of all JConsole tabs
    synchronized List<Tab> getTabs() {
        ArrayList<Tab> list = new ArrayList<Tab>();
        int n = getTabCount();
        for (int i = 0; i < n; i++) {
            Component c = getComponentAt(i);
            if (c instanceof Tab) {
                list.add((Tab) c);
            }
        }
        return list;
    }

    private void startUpdateTimer() {
        if (timer != null) {
            timer.cancel();
        }
        TimerTask timerTask = new TimerTask() {

            public void run() {
                update();
            }
        };
        String timerName = "Timer-" + getConnectionName();
        timer = new Timer(timerName, true);
        timer.schedule(timerTask, 0, updateInterval);
    }

    // Call on EDT
    private void vmPanelDied() {
        disconnect();

        if (userDisconnected) {
            userDisconnected = false;
            return;
        }

        JOptionPane optionPane;
        String msgTitle, msgExplanation, buttonStr;

        if (wasConnected) {
            wasConnected = false;
            msgTitle = Messages.CONNECTION_LOST1;
            msgExplanation = Resources.format(Messages.CONNECTING_TO2, getConnectionName());
            buttonStr = Messages.RECONNECT;
        } else if (shouldUseSSL) {
            msgTitle = Messages.CONNECTION_FAILED_SSL1;
            msgExplanation = Resources.format(Messages.CONNECTION_FAILED_SSL2, getConnectionName());
            buttonStr = Messages.INSECURE;
        } else {
            msgTitle = Messages.CONNECTION_FAILED1;
            msgExplanation = Resources.format(Messages.CONNECTION_FAILED2, getConnectionName());
            buttonStr = Messages.CONNECT;
        }

        optionPane =
                SheetDialog.showOptionDialog(this,
                "<html><h3>" + msgTitle + "</h3>" +
                "<b>" + msgExplanation + "</b>",
                JOptionPane.DEFAULT_OPTION,
                JOptionPane.WARNING_MESSAGE, null,
                new String[]{buttonStr, Messages.CANCEL},
                0);

        optionPane.addPropertyChangeListener(new PropertyChangeListener() {

            public void propertyChange(PropertyChangeEvent event) {
                if (event.getPropertyName().equals(JOptionPane.VALUE_PROPERTY)) {
                    Object value = event.getNewValue();

                    if (value == Messages.RECONNECT || value == Messages.CONNECT) {
                        connect();
                    } else if (value == Messages.INSECURE) {
                        shouldUseSSL = false;
                        connect();
                    } else if (!everConnected) {
                        try {
                            getFrame().setClosed(true);
                        } catch (PropertyVetoException ex) {
                        // Should not happen, but can be ignored.
                        }
                    }
                }
            }
        });
    }

    // Note: This method is called on a TimerTask thread. Any GUI manipulation
    // must be performed with invokeLater() or invokeAndWait().
    private Object lockObject = new Object();

    private void update() {
        synchronized (lockObject) {
            if (!isConnected()) {
                if (wasConnected) {
                    EventQueue.invokeLater(new Runnable() {

                        public void run() {
                            vmPanelDied();
                        }
                    });
                }
                wasConnected = false;
                return;
            } else {
                wasConnected = true;
                everConnected = true;
            }
            proxyClient.flush();
            List<Tab> tabs = getTabs();
            final int n = tabs.size();
            for (int i = 0; i < n; i++) {
                final int index = i;
                try {
                    if (!proxyClient.isDead()) {
                        // Update tab
                        //
                        tabs.get(index).update();
                        // Enable tab on initial update
                        //
                        if (initialUpdate) {
                            EventQueue.invokeLater(new Runnable() {

                                public void run() {
                                    setEnabledAt(index, true);
                                }
                            });
                        }
                    }
                } catch (Exception e) {
                    // Disable tab on initial update
                    //
                    if (initialUpdate) {
                        EventQueue.invokeLater(new Runnable() {
                            public void run() {
                                setEnabledAt(index, false);
                            }
                        });
                    }
                }
            }

            // plugin GUI update
            for (ExceptionSafePlugin p : plugins.keySet()) {
                SwingWorker<?, ?> sw = p.newSwingWorker();
                SwingWorker<?, ?> prevSW = plugins.get(p);
                // schedule SwingWorker to run only if the previous
                // SwingWorker has finished its task and it hasn't started.
                if (prevSW == null || prevSW.isDone()) {
                    if (sw == null || sw.getState() == SwingWorker.StateValue.PENDING) {
                        plugins.put(p, sw);
                        if (sw != null) {
                            p.executeSwingWorker(sw);
                        }
                    }
                }
            }

            // Set the first enabled tab in the tab's list
            // as the selected tab on initial update
            //
            if (initialUpdate) {
                EventQueue.invokeLater(new Runnable() {
                    public void run() {
                        // Select first enabled tab if current tab isn't.
                        int index = getSelectedIndex();
                        if (index < 0 || !isEnabledAt(index)) {
                            for (int i = 0; i < n; i++) {
                                if (isEnabledAt(i)) {
                                    setSelectedIndex(i);
                                    break;
                                }
                            }
                        }
                    }
                });
                initialUpdate = false;
            }
        }
    }

    public String getHostName() {
        return hostName;
    }

    public int getPort() {
        return port;
    }

    public String getUserName() {
        return userName;
    }

    public String getUrl() {
        return url;
    }

    public String getPassword() {
        return password;
    }

    public String getConnectionName() {
        return proxyClient.connectionName();
    }

    public String getDisplayName() {
        return proxyClient.getDisplayName();
    }

    static class TabInfo {

        Class<? extends Tab> tabClass;
        String name;
        boolean tabVisible;

        TabInfo(Class<? extends Tab> tabClass, String name, boolean tabVisible) {
            this.tabClass = tabClass;
            this.name = name;
            this.tabVisible = tabVisible;
        }
    }

    private void createPluginTabs() {
        // add plugin tabs if not done
        if (!pluginTabsAdded) {
            for (JConsolePlugin p : plugins.keySet()) {
                Map<String, JPanel> tabs = p.getTabs();
                for (Map.Entry<String, JPanel> e : tabs.entrySet()) {
                    addTab(e.getKey(), e.getValue());
                }
            }
            pluginTabsAdded = true;
        }
    }

    private void fireConnectedChange(boolean connected) {
        for (Tab tab : getTabs()) {
            tab.firePropertyChange(JConsoleContext.CONNECTION_STATE_PROPERTY, !connected, connected);
        }
    }
}
