/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you 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 org.apache.logging.log4j.jmx.gui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;

import javax.management.InstanceNotFoundException;
import javax.management.JMException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanServerDelegate;
import javax.management.MBeanServerNotification;
import javax.management.MalformedObjectNameException;
import javax.management.Notification;
import javax.management.NotificationFilterSupport;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JToggleButton;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.WindowConstants;

import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean;
import org.apache.logging.log4j.core.jmx.Server;
import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean;

/**
 * Swing GUI that connects to a Java process via JMX and allows the user to view
 * and modify the Log4j 2 configuration, as well as monitor status logs.
 *
 * @see <a href=
 *      "http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html"
 *      >http://docs.oracle.com/javase/6/docs/technotes/guides/management/
 *      jconsole.html</a >
 */
public class ClientGui extends JPanel implements NotificationListener {
    private static final long serialVersionUID = -253621277232291174L;
    private static final int INITIAL_STRING_WRITER_SIZE = 1024;
    private final Client client;
    private final Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<>();
    private final Map<ObjectName, JTextArea> statusLogTextAreaMap = new HashMap<>();
    private JTabbedPane tabbedPaneContexts;

    public ClientGui(final Client client) throws IOException, JMException {
        this.client = Objects.requireNonNull(client, "client");
        createWidgets();
        populateWidgets();

        // register for Notifications if LoggerContext MBean was added/removed
        final ObjectName addRemoveNotifs = MBeanServerDelegate.DELEGATE_NAME;
        final NotificationFilterSupport filter = new NotificationFilterSupport();
        filter.enableType(Server.DOMAIN); // only interested in Log4J2 MBeans
        client.getConnection().addNotificationListener(addRemoveNotifs, this, null, null);
    }

    private void createWidgets() {
        tabbedPaneContexts = new JTabbedPane();
        this.setLayout(new BorderLayout());
        this.add(tabbedPaneContexts, BorderLayout.CENTER);
    }

    private void populateWidgets() throws IOException, JMException {
        for (final LoggerContextAdminMBean ctx : client.getLoggerContextAdmins()) {
            addWidgetForLoggerContext(ctx);
        }
    }

    private void addWidgetForLoggerContext(final LoggerContextAdminMBean ctx) throws MalformedObjectNameException,
            IOException, InstanceNotFoundException {
        final JTabbedPane contextTabs = new JTabbedPane();
        contextObjNameToTabbedPaneMap.put(ctx.getObjectName(), contextTabs);
        tabbedPaneContexts.addTab("LoggerContext: " + ctx.getName(), contextTabs);

        final String contextName = ctx.getName();
        final StatusLoggerAdminMBean status = client.getStatusLoggerAdmin(contextName);
        if (status != null) {
            final JTextArea text = createTextArea();
            final String[] messages = status.getStatusDataHistory();
            for (final String message : messages) {
                text.append(message + '\n');
            }
            statusLogTextAreaMap.put(ctx.getObjectName(), text);
            registerListeners(status);
            final JScrollPane scroll = scroll(text);
            contextTabs.addTab("StatusLogger", scroll);
        }

        final ClientEditConfigPanel editor = new ClientEditConfigPanel(ctx);
        contextTabs.addTab("Configuration", editor);
    }

    private void removeWidgetForLoggerContext(final ObjectName loggerContextObjName) throws JMException, IOException {
        final Component tab = contextObjNameToTabbedPaneMap.get(loggerContextObjName);
        if (tab != null) {
            tabbedPaneContexts.remove(tab);
        }
        statusLogTextAreaMap.remove(loggerContextObjName);
        final ObjectName objName = client.getStatusLoggerObjectName(loggerContextObjName);
        try {
            // System.out.println("Remove listener for " + objName);
            client.getConnection().removeNotificationListener(objName, this);
        } catch (final ListenerNotFoundException ignored) {
        }
    }

    private JTextArea createTextArea() {
        final JTextArea result = new JTextArea();
        result.setEditable(false);
        result.setBackground(this.getBackground());
        result.setForeground(Color.black);
        result.setFont(new Font(Font.MONOSPACED, Font.PLAIN, result.getFont().getSize()));
        result.setWrapStyleWord(true);
        return result;
    }

    private JScrollPane scroll(final JTextArea text) {
        final JToggleButton toggleButton = new JToggleButton();
        toggleButton.setAction(new AbstractAction() {
            private static final long serialVersionUID = -4214143754637722322L;

            @Override
            public void actionPerformed(final ActionEvent e) {
                final boolean wrap = toggleButton.isSelected();
                text.setLineWrap(wrap);
            }
        });
        toggleButton.setToolTipText("Toggle line wrapping");
        final JScrollPane scrollStatusLog = new JScrollPane(text, //
                ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, //
                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
        scrollStatusLog.setCorner(ScrollPaneConstants.LOWER_RIGHT_CORNER, toggleButton);
        return scrollStatusLog;
    }

    private void registerListeners(final StatusLoggerAdminMBean status) throws InstanceNotFoundException,
            MalformedObjectNameException, IOException {
        final NotificationFilterSupport filter = new NotificationFilterSupport();
        filter.enableType(StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE);
        final ObjectName objName = status.getObjectName();
        // System.out.println("Add listener for " + objName);
        client.getConnection().addNotificationListener(objName, this, filter, status.getContextName());
    }

    @Override
    public void handleNotification(final Notification notif, final Object paramObject) {
        SwingUtilities.invokeLater(() -> { // LOG4J2-538
            handleNotificationInAwtEventThread(notif, paramObject);
        });
    }

    private void handleNotificationInAwtEventThread(final Notification notif, final Object paramObject) {
        if (StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE.equals(notif.getType())) {
            if (!(paramObject instanceof ObjectName)) {
                handle("Invalid notification object type", new ClassCastException(paramObject.getClass().getName()));
                return;
            }
            final ObjectName param = (ObjectName) paramObject;
            final JTextArea text = statusLogTextAreaMap.get(param);
            if (text != null) {
                text.append(notif.getMessage() + '\n');
            }
            return;
        }
        if (notif instanceof MBeanServerNotification) {
            final MBeanServerNotification mbsn = (MBeanServerNotification) notif;
            final ObjectName mbeanName = mbsn.getMBeanName();
            if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notif.getType())) {
                onMBeanRegistered(mbeanName);
            } else if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(notif.getType())) {
                onMBeanUnregistered(mbeanName);
            }
        }
    }

    /**
     * Called every time a Log4J2 MBean was registered in the MBean server.
     *
     * @param mbeanName ObjectName of the registered Log4J2 MBean
     */
    private void onMBeanRegistered(final ObjectName mbeanName) {
        if (client.isLoggerContext(mbeanName)) {
            try {
                final LoggerContextAdminMBean ctx = client.getLoggerContextAdmin(mbeanName);
                addWidgetForLoggerContext(ctx);
            } catch (final Exception ex) {
                handle("Could not add tab for new MBean " + mbeanName, ex);
            }
        }
    }

    /**
     * Called every time a Log4J2 MBean was unregistered from the MBean server.
     *
     * @param mbeanName ObjectName of the unregistered Log4J2 MBean
     */
    private void onMBeanUnregistered(final ObjectName mbeanName) {
        if (client.isLoggerContext(mbeanName)) {
            try {
                removeWidgetForLoggerContext(mbeanName);
            } catch (final Exception ex) {
                handle("Could not remove tab for " + mbeanName, ex);
            }
        }
    }

    private void handle(final String msg, final Exception ex) {
        System.err.println(msg);
        ex.printStackTrace();

        final StringWriter sw = new StringWriter(INITIAL_STRING_WRITER_SIZE);
        ex.printStackTrace(new PrintWriter(sw));
        JOptionPane.showMessageDialog(this, sw.toString(), msg, JOptionPane.ERROR_MESSAGE);
    }

    /**
     * Connects to the specified location and shows this panel in a window.
     * <p>
     * Useful links:
     * http://www.componative.com/content/controller/developer/insights
     * /jconsole3/
     *
     * @param args must have at least one parameter, which specifies the
     *            location to connect to. Must be of the form {@code host:port}
     *            or {@code service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi}
     *            or
     *            {@code service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi}
     * @throws Exception if anything goes wrong
     */
    public static void main(final String[] args) throws Exception {
        if (args.length < 1) {
            usage();
            return;
        }
        String serviceUrl = args[0];
        if (!serviceUrl.startsWith("service:jmx")) {
            serviceUrl = "service:jmx:rmi:///jndi/rmi://" + args[0] + "/jmxrmi";
        }
        final JMXServiceURL url = new JMXServiceURL(serviceUrl);
        final Properties props = System.getProperties();
        final Map<String, String> paramMap = new HashMap<>(props.size());
        for (final String key : props.stringPropertyNames()) {
            paramMap.put(key, props.getProperty(key));
        }
        final JMXConnector connector = JMXConnectorFactory.connect(url, paramMap);
        final Client client = new Client(connector);
        final String title = "Log4j JMX Client - " + url;

        SwingUtilities.invokeLater(() -> {
            installLookAndFeel();
            try {
                final ClientGui gui = new ClientGui(client);
                final JFrame frame = new JFrame(title);
                frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
                frame.getContentPane().add(gui, BorderLayout.CENTER);
                frame.pack();
                frame.setVisible(true);
            } catch (final Exception ex) {
                // if console is visible, print error so that
                // the stack trace remains visible after error dialog is
                // closed
                ex.printStackTrace();

                // show error in dialog: there may not be a console window
                // visible
                final StringWriter sr = new StringWriter();
                ex.printStackTrace(new PrintWriter(sr));
                JOptionPane.showMessageDialog(null, sr.toString(), "Error", JOptionPane.ERROR_MESSAGE);
            }
        });
    }

    private static void usage() {
        final String me = ClientGui.class.getName();
        System.err.println("Usage: java " + me + " <host>:<port>");
        System.err.println("   or: java " + me + " service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi");
        final String longAdr = " service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi";
        System.err.println("   or: java " + me + longAdr);
    }

    private static void installLookAndFeel() {
        try {
            for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    UIManager.setLookAndFeel(info.getClassName());
                    return;
                }
            }
        } catch (final Exception ex) {
            ex.printStackTrace();
        }
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }
}
