/*
 * Copyright (c) 2007, 2018, 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 org.graalvm.visualvm.modules.mbeans;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import javax.swing.tree.*;
import java.awt.Font;

import java.text.SimpleDateFormat;

import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.*;
import java.awt.Dimension;
import java.util.*;
import java.io.*;
import java.lang.reflect.Array;

import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.*;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.TabularData;

class XMBeanNotifications extends JTable implements NotificationListener {
    
    private final static String[] columnNames =  {
        Resources.getText("LBL_TimeStamp"), // NOI18N
        Resources.getText("LBL_Type"), // NOI18N
        Resources.getText("LBL_UserData"), // NOI18N
        Resources.getText("LBL_SeqNum"), // NOI18N
        Resources.getText("LBL_Message"), // NOI18N
        Resources.getText("LBL_Event"), // NOI18N
        Resources.getText("LBL_Source")}; // NOI18N
    
    private final static Logger LOGGER = Logger.getLogger(XMBeanNotifications.class.getName());
    
    private HashMap<ObjectName, XMBeanNotificationsListener> listeners =
            new HashMap<ObjectName, XMBeanNotificationsListener>();
    private volatile boolean subscribed;
    private XMBeanNotificationsListener currentListener;
    public final static String NOTIFICATION_RECEIVED_EVENT =
            "jconsole.xnotification.received"; // NOI18N
    private List<NotificationListener> notificationListenersList;
    private volatile boolean enabled;
    private Font normalFont, boldFont;
    private int rowMinHeight = -1;
    private TableCellEditor userDataEditor = new UserDataCellEditor();
    private NotifMouseListener mouseListener = new NotifMouseListener();
    private SimpleDateFormat timeFormater = new SimpleDateFormat("HH:mm:ss:SSS"); // NOI18N
    private static TableCellEditor editor =
            new Utils.ReadOnlyTableCellEditor(new JTextField());
    
    public XMBeanNotifications() {
        super(new TableSorter(columnNames,0));
        setColumnSelectionAllowed(false);
        setRowSelectionAllowed(false);
        getTableHeader().setReorderingAllowed(false);
        ArrayList<NotificationListener> l =
                new ArrayList<NotificationListener>(1);
        notificationListenersList = Collections.synchronizedList(l);
        
        addMouseListener(mouseListener);
        
        TableColumnModel colModel = getColumnModel();
        colModel.getColumn(0).setPreferredWidth(45);
        colModel.getColumn(1).setPreferredWidth(50);
        colModel.getColumn(2).setPreferredWidth(50);
        colModel.getColumn(3).setPreferredWidth(40);
        colModel.getColumn(4).setPreferredWidth(50);
        colModel.getColumn(5).setPreferredWidth(50);
        setColumnEditors();
        addKeyListener(new Utils.CopyKeyAdapter());
    }
    
    // Call on EDT
    public void cancelCellEditing() {
        TableCellEditor tce = getCellEditor();
        if (tce != null) {
            tce.cancelCellEditing();
        }
    }
    
    // Call on EDT
    public void stopCellEditing() {
        TableCellEditor tce = getCellEditor();
        if (tce != null) {
            tce.stopCellEditing();
        }
    }
    
    // Call on EDT
    @Override
    public boolean isCellEditable(int row, int col) {
        UserDataCell cell = getUserDataCell(row, col);
        if (cell != null) {
            return cell.isMaximized();
        }
        return true;
    }
    
    // Call on EDT
    @Override
    public void setValueAt(Object value, int row, int column) {
    }
    
    // Call on EDT
    @Override
    public synchronized Component prepareRenderer(
            TableCellRenderer renderer, int row, int column) {
        //In case we have a repaint thread that is in the process of
        //repainting an obsolete table, just ignore the call.
        //It can happen when MBean selection is switched at a very quick rate
        if (row >= getRowCount())
            return null;
        
        Component comp = super.prepareRenderer(renderer, row, column);
        
        if (normalFont == null) {
            normalFont = comp.getFont();
            boldFont = normalFont.deriveFont(Font.BOLD);
        }
        UserDataCell cell = getUserDataCell(row, 2);
        if (column == 2 && cell != null) {
            comp.setFont(boldFont);
            int size = cell.getHeight();
            if (size > 0) {
                if(getRowHeight(row) != size)
                    setRowHeight(row, size);
            }
        } else {
            comp.setFont(normalFont);
        }
        
        return comp;
    }
    
    // Call on EDT
    @Override
    public synchronized TableCellRenderer getCellRenderer(int row, int column) {
        //In case we have a repaint thread that is in the process of
        //repainting an obsolete table, just ignore the call.
        //It can happen when MBean selection is switched at a very quick rate
        if (row >= getRowCount())
            return null;
        
        DefaultTableCellRenderer renderer;
        String toolTip = null;
        UserDataCell cell = getUserDataCell(row, column);
        if (cell != null && cell.isInited()) {
            renderer = (DefaultTableCellRenderer) cell.getRenderer();
        } else {
            renderer =
                    (DefaultTableCellRenderer) super.getCellRenderer(row, column);
        }
        
        if (cell != null)
            toolTip = Resources.getText("LBL_DoubleClickToExpandCollapse") + // NOI18N
                    ". " + cell.toString(); // NOI18N
        else {
            Object val =
                    ((DefaultTableModel) getModel()).getValueAt(row, column);
            if (val != null)
                toolTip = val.toString();
        }
        
        renderer.setToolTipText(toolTip);
        
        return renderer;
    }
    
    // Call on EDT
    private UserDataCell getUserDataCell(int row, int column) {
        Object obj = ((DefaultTableModel) getModel()).getValueAt(row,column);
        if (obj instanceof UserDataCell) return (UserDataCell) obj;
        return null;
    }
    
    synchronized void dispose() {
        listeners.clear();
    }
    
    public long getReceivedNotifications(XMBean mbean) {
        XMBeanNotificationsListener listener =
                listeners.get(mbean.getObjectName());
        if (listener == null)
            return 0;
        else
            return listener.getReceivedNotifications();
    }
    
    public synchronized boolean clearCurrentNotifications() {
        emptyTable();
        if (currentListener != null) {
            currentListener.clear();
            return true;
        } else
            return false;
    }
    
    public synchronized boolean unregisterListener(DefaultMutableTreeNode node) {
        XMBean mbean = (XMBean) ((XNodeInfo) node.getUserObject()).getData();
        return unregister(mbean.getObjectName());
    }
    
    public synchronized void registerListener(DefaultMutableTreeNode node)
    throws InstanceNotFoundException, IOException {
        XMBean mbean = (XMBean) ((XNodeInfo) node.getUserObject()).getData();
        if (!subscribed) {
            try {
                mbean.getMBeanServerConnection().addNotificationListener(
                        MBeanServerDelegate.DELEGATE_NAME, this, null, null);
                subscribed = true;
            } catch (Exception e) {
                LOGGER.log(Level.SEVERE, "Error adding listener for delegate", e); // NOI18N
            }
        }
        XMBeanNotificationsListener listener =
                listeners.get(mbean.getObjectName());
        if (listener == null) {
            listener = new XMBeanNotificationsListener(
                    this, mbean, node, columnNames);
            listeners.put(mbean.getObjectName(), listener);
        } else {
            if (!listener.isRegistered()) {
                emptyTable();
                listener.register(node);
            }
        }
        enabled = true;
        currentListener = listener;
    }
    
    public synchronized void handleNotification(
            Notification notif, Object handback) {
        try {
            if (notif instanceof MBeanServerNotification) {
                ObjectName mbean =
                        ((MBeanServerNotification) notif).getMBeanName();
                if (notif.getType().indexOf("JMX.mbean.unregistered") >= 0) { // NOI18N
                    unregister(mbean);
                }
            }
        } catch(Exception e) {
            LOGGER.log(Level.SEVERE, "Error unregistering notification", e); // NOI18N
        }
    }
    
    public synchronized void disableNotifications() {
        emptyTable();
        currentListener = null;
        enabled = false;
    }
    
    private synchronized boolean unregister(ObjectName mbean) {
        XMBeanNotificationsListener listener = listeners.get(mbean);
        if (listener != null && listener.isRegistered()) {
            listener.unregister();
            return true;
        } else
            return false;
    }
    
    public void addNotificationsListener(NotificationListener nl) {
        notificationListenersList.add(nl);
    }
    
    public void removeNotificationsListener(NotificationListener nl) {
        notificationListenersList.remove(nl);
    }
    
    // Call on EDT
    void fireNotificationReceived(
            XMBeanNotificationsListener listener, XMBean mbean,
            DefaultMutableTreeNode node, Object[] rowData, long received) {
        if (enabled) {
            DefaultTableModel tableModel = (DefaultTableModel) getModel();
            if (listener == currentListener) {
                tableModel.insertRow(0, rowData);
                repaint();
            }
        }
        Notification notif =
                new Notification(NOTIFICATION_RECEIVED_EVENT, this, 0);
        notif.setUserData(received);
        for (NotificationListener nl : notificationListenersList)
            nl.handleNotification(notif, node);
    }
    
    // Call on EDT
    private void updateModel(List<Object[]> data) {
        emptyTable();
        DefaultTableModel tableModel = (DefaultTableModel) getModel();
        for (Object[] rowData : data)
            tableModel.addRow(rowData);
    }
    
    public synchronized boolean isListenerRegistered(XMBean mbean) {
        XMBeanNotificationsListener listener =
                listeners.get(mbean.getObjectName());
        if (listener == null) return false;
        return listener.isRegistered();
    }
    
    // Call on EDT
    public synchronized void loadNotifications(XMBean mbean) {
        XMBeanNotificationsListener listener =
                listeners.get(mbean.getObjectName());
        emptyTable();
        if (listener != null) {
            enabled = true;
            List<Object[]> data = listener.getData();
            updateModel(data);
            currentListener = listener;
            validate();
            repaint();
        } else
            enabled = false;
    }
    
    // Call on EDT
    private void setColumnEditors() {
        TableColumnModel tcm = getColumnModel();
        for (int i = 0; i < columnNames.length; i++) {
            TableColumn tc = tcm.getColumn(i);
            if (i == 2) {
                tc.setCellEditor(userDataEditor);
            } else {
                tc.setCellEditor(editor);
            }
        }
    }
    
    // Call on EDT
    public boolean isTableEditable() {
        return true;
    }
    
    // Call on EDT
    public synchronized void emptyTable() {
        DefaultTableModel model = (DefaultTableModel) getModel();
        //invalidate();
        while (model.getRowCount() > 0)
            model.removeRow(0);
        validate();
    }
    
    // Call on EDT
    synchronized void updateUserDataCell(int row, int col) {
        Object obj = getModel().getValueAt(row, 2);
        if (obj instanceof UserDataCell) {
            UserDataCell cell = (UserDataCell) obj;
            if (!cell.isInited()) {
                if (rowMinHeight == -1)
                    rowMinHeight = getRowHeight(row);
                cell.init(super.getCellRenderer(row, col), rowMinHeight);
            }
            
            cell.switchState();
            setRowHeight(row, cell.getHeight());
            
            if(!cell.isMaximized()) {
                cancelCellEditing();
                //Back to simple editor.
                editCellAt(row, 2);
            }
            
            invalidate();
            repaint();
        }
    }
    
    class UserDataCellRenderer extends DefaultTableCellRenderer {
        Component comp;
        UserDataCellRenderer(Component comp) {
            this.comp = comp;
            Dimension d = comp.getPreferredSize();
            if (d.getHeight() > 200) {
                comp.setPreferredSize(new Dimension((int) d.getWidth(), 200));
            }
        }
        
        @Override
        public Component getTableCellRendererComponent(
                JTable table,
                Object value,
                boolean isSelected,
                boolean hasFocus,
                int row,
                int column) {
            return comp;
        }
        
        public Component getComponent() {
            return comp;
        }
        
    }
    
    class UserDataCell {
        TableCellRenderer minRenderer;
        UserDataCellRenderer maxRenderer;
        int minHeight;
        boolean minimized = true;
        boolean init = false;
        Object userData;
        UserDataCell(Object userData, Component max) {
            this.userData = userData;
            this.maxRenderer = new UserDataCellRenderer(max);
            
        }
        
        @Override
        public String toString() {
            if (userData == null) return null;
            if (userData.getClass().isArray()) {
                String name =
                        Utils.getArrayClassName(userData.getClass().getName());
                int length = Array.getLength(userData);
                return name + "[" + length +"]"; // NOI18N
            }
            
            if (userData instanceof CompositeData ||
                    userData instanceof TabularData)
                return userData.getClass().getName();
            
            return userData.toString();
        }
        
        boolean isInited() {
            return init;
        }
        
        void init(TableCellRenderer minRenderer, int minHeight) {
            this.minRenderer = minRenderer;
            this.minHeight = minHeight;
            init = true;
        }
        
        void switchState() {
            minimized = !minimized;
        }
        boolean isMaximized() {
            return !minimized;
        }
        void minimize() {
            minimized = true;
        }
        
        void maximize() {
            minimized = false;
        }
        
        int getHeight() {
            if (minimized) return minHeight;
            else
                return (int) maxRenderer.getComponent().
                        getPreferredSize().getHeight();
        }
        
        TableCellRenderer getRenderer() {
            if (minimized) return minRenderer;
            else return maxRenderer;
        }
    }
    
    class NotifMouseListener extends MouseAdapter {
        
        @Override
        public void mousePressed(MouseEvent e) {
            if (e.getButton() == MouseEvent.BUTTON1) {
                if (e.getClickCount() >= 2) {
                    int row = XMBeanNotifications.this.getSelectedRow();
                    int col = XMBeanNotifications.this.getSelectedColumn();
                    if (col != 2) return;
                    if (col == -1 || row == -1) return;
                    
                    XMBeanNotifications.this.updateUserDataCell(row, col);
                }
            }
        }
    }
    
    class UserDataCellEditor extends XTextFieldEditor {
        // implements javax.swing.table.TableCellEditor
        @Override
        public Component getTableCellEditorComponent(
                JTable table,
                Object value,
                boolean isSelected,
                int row,
                int column) {
            Object val = value;
            if (column == 2) {
                Object obj = getModel().getValueAt(row, column);
                if (obj instanceof UserDataCell) {
                    UserDataCell cell = (UserDataCell) obj;
                    if (cell.getRenderer() instanceof UserDataCellRenderer) {
                        UserDataCellRenderer zr =
                                (UserDataCellRenderer) cell.getRenderer();
                        return zr.getComponent();
                    }
                } else {
                    Component comp = super.getTableCellEditorComponent(
                            table, val, isSelected, row, column);
                    textField.setEditable(false);
                    return comp;
                }
            }
            return super.getTableCellEditorComponent(
                    table,
                    val,
                    isSelected,
                    row,
                    column);
        }
        @Override
        public boolean stopCellEditing() {
            int editingRow = getEditingRow();
            int editingColumn = getEditingColumn();
            if (editingColumn == 2) {
                Object obj = getModel().getValueAt(editingRow, editingColumn);
                if (obj instanceof UserDataCell) {
                    UserDataCell cell = (UserDataCell) obj;
                    if (cell.isMaximized()) {
                        cancelCellEditing();
                        return true;
                    }
                }
            }
            return super.stopCellEditing();
        }
    }
    
    class XMBeanNotificationsListener implements NotificationListener {
        
        private String[] columnNames;
        private XMBean mbean;
        private DefaultMutableTreeNode node;
        private volatile long received;
        private XMBeanNotifications notifications;
        private volatile boolean unregistered;
        private ArrayList<Object[]> data = new ArrayList<Object[]>();
        
        public XMBeanNotificationsListener(
                XMBeanNotifications notifications,
                XMBean mbean,
                DefaultMutableTreeNode node,
                String[] columnNames) {
            this.notifications = notifications;
            this.mbean = mbean;
            this.node = node;
            this.columnNames = columnNames;
            register(node);
        }
        
        public synchronized List<Object[]> getData() {
            return data;
        }
        
        public synchronized void clear() {
            data.clear();
            received = 0;
        }
        
        public synchronized boolean isRegistered() {
            return !unregistered;
        }
        
        public synchronized void unregister() {
            try {
                mbean.getMBeanServerConnection().removeNotificationListener(
                        mbean.getObjectName(), this, null, null);
            } catch (Exception e) {
                LOGGER.log(Level.SEVERE, "Error removing listener", e); // NOI18N
            }
            unregistered = true;
        }
        
        public synchronized long getReceivedNotifications() {
            return received;
        }
        
        public synchronized void register(DefaultMutableTreeNode node) {
            clear();
            this.node = node;
            try {
                mbean.getMBeanServerConnection().addNotificationListener(
                        mbean.getObjectName(), this, null, null);
                unregistered = false;
            } catch (Exception e) {
                LOGGER.log(Level.SEVERE, "Error adding listener", e); // NOI18N
            }
        }
        
        public synchronized void handleNotification(
                final Notification n, Object hb) {
            EventQueue.invokeLater(new Runnable() {
                public void run() {
                    synchronized (XMBeanNotificationsListener.this) {
                        try {
                            if (unregistered) return;
                            Date receivedDate = new Date(n.getTimeStamp());
                            String time = timeFormater.format(receivedDate);
                            
                            Object userData = n.getUserData();
                            Component comp = null;
                            UserDataCell cell = null;
                            if ((comp = XDataViewer.createNotificationViewer(userData)) != null) {
                                XDataViewer.registerForMouseEvent(comp, mouseListener);
                                cell = new UserDataCell(userData, comp);
                            }
                            
                            Object[] rowData = {
                                time,
                                n.getType(),
                                (cell == null ? userData : cell),
                                n.getSequenceNumber(),
                                n.getMessage(),
                                n,
                                n.getSource()
                            };
                            received++;
                            data.add(0, rowData);
                            
                            notifications.fireNotificationReceived(
                                    XMBeanNotificationsListener.this,
                                    mbean, node, rowData, received);
                        } catch (Exception e) {
                            LOGGER.log(Level.SEVERE, "Error handling notification", e); // NOI18N
                        }
                    }
                }
            });
        }
    }
}
