/**
 * 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.camel.jmxconnect;


import java.io.IOException;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import javax.management.ListenerNotFoundException;
import javax.management.MBeanServerConnection;
import javax.management.NotificationBroadcaster;
import javax.management.NotificationBroadcasterSupport;
import javax.management.NotificationEmitter;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.remote.JMXConnectionNotification;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServerErrorException;
import javax.management.remote.JMXServiceURL;
import javax.security.auth.Subject;

import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.spring.remoting.CamelProxyFactoryBean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <p>
 * The client end of a JMX API connector. An object of this type can be used to establish a connection to a connector
 * server.
 * </p>
 * <p/>
 * <p>
 * A newly-created object of this type is unconnected. Its {@link #connect()} method must be called before it can
 * be used. However, objects created by {@link JMXConnectorFactory#connect(JMXServiceURL, Map)
 * JMXConnectorFactory.connect} are already connected.
 * </p>
 *
 * @version $Revision: 2858 $
 */
public class CamelJmxConnector implements JMXConnector, CamelContextAware {
    private static final Log LOG = LogFactory.getLog(JMXConnector.class);
    private NotificationBroadcasterSupport connectionNotifier = new NotificationBroadcasterSupport();
    private AtomicLong notificationNumber = new AtomicLong();
    private Map env;
    private String endpointUri;
    private CamelProxyFactoryBean proxy;
    private MBeanCamelServerConnectionClient client;
    private boolean connected;
    private CamelContext camelContext;
    private String connectionId;

    public CamelJmxConnector(Map env, String endpointUri) {
        this.env = env;
        this.endpointUri = endpointUri;
    }

    public CamelJmxConnector(Map env, JMXServiceURL url) throws IOException {
        this(env, CamelJmxConnectorSupport.getEndpointUri(url, "camel"));
        // set any props in the url
        // TODO
        // populateProperties(this, endpointUri);
    }

    /**
     * <p>
     * Establishes the connection to the connector server. This method is equivalent to {@link #connect(Map)
     * connect(null)}.
     * </p>
     *
     * @throws IOException       if the connection could not be made because of a communication problem.
     * @throws SecurityException if the connection could not be made for security reasons.
     */
    public void connect() throws IOException {
        connect(this.env);
    }

    /**
     * <p>
     * Establishes the connection to the connector server.
     * </p>
     * <p/>
     * <p>
     * If <code>connect</code> has already been called successfully on this object, calling it again has no effect.
     * If, however, {@link #close} was called after <code>connect</code>, the new <code>connect</code> will throw
     * an <code>IOException</code>.
     * <p>
     * <p/>
     * <p>
     * Otherwise, either <code>connect</code> has never been called on this object, or it has been called but produced
     * an exception. Then calling <code>connect</code> will attempt to establish a connection to the connector server.
     * </p>
     *
     * @param env the properties of the connection. Properties in this map override properties in the map specified when
     *            the <code>JMXConnector</code> was created, if any. This parameter can be null, which is equivalent
     *            to an empty map.
     * @throws IOException       if the connection could not be made because of a communication problem.
     * @throws SecurityException if the connection could not be made for security reasons.
     */
    public void connect(Map env) throws IOException {
        if (!connected) {
            try {
                proxy = new CamelProxyFactoryBean();
                proxy.setCamelContext(getCamelContext());
                proxy.setServiceInterface(javax.management.MBeanServerConnection.class);
                proxy.setServiceInterface(MBeanCamelServerConnection.class);
                proxy.setServiceUrl(endpointUri);
                proxy.afterPropertiesSet();

                client = new MBeanCamelServerConnectionClient((MBeanCamelServerConnection) proxy.getObject() /* TODO */);
                connectionId = client.generateId();

                // TODO we need to establish a replyToEndpoint and inform the server!

                sendConnectionNotificationOpened();
            } catch (Exception e) {
                LOG.error("Failed to connect: " + e, e);
                IOException ioe = new IOException(e.getMessage());
                throw ioe;
            }
            connected = true;
        }
    }

    /**
     * <p>
     * Returns an <code>MBeanServerConnection</code> object representing a remote MBean server. For a given
     * <code>JMXConnector</code>, two successful calls to this method will usually return the same
     * <code>MBeanServerConnection</code> object, though this is not required.
     * </p>
     * <p/>
     * <p>
     * For each method in the returned <code>MBeanServerConnection</code>, calling the method causes the
     * corresponding method to be called in the remote MBean server. The value returned by the MBean server method is
     * the value returned to the client. If the MBean server method produces an <code>Exception</code>, the same
     * <code>Exception</code> is seen by the client. If the MBean server method, or the attempt to call it, produces
     * an <code>Error</code>, the <code>Error</code> is wrapped in a {@link JMXServerErrorException}, which is
     * seen by the client.
     * </p>
     * <p/>
     * <p>
     * Calling this method is equivalent to calling
     * {@link #getMBeanServerConnection(Subject) getMBeanServerConnection(null)} meaning that no delegation subject is
     * specified and that all the operations called on the <code>MBeanServerConnection</code> must use the
     * authenticated subject, if any.
     * </p>
     *
     * @return an object that implements the <code>MBeanServerConnection</code> interface by forwarding its methods to
     *         the remote MBean server.
     */
    public MBeanServerConnection getMBeanServerConnection() {
        return client;
    }

    /**
     * <p>
     * Returns an <code>MBeanServerConnection</code> object representing a remote MBean server on which operations are
     * performed on behalf of the supplied delegation subject. For a given <code>JMXConnector</code> and
     * <code>Subject</code>, two successful calls to this method will usually return the same
     * <code>MBeanServerConnection</code> object, though this is not required.
     * </p>
     * <p/>
     * <p>
     * For each method in the returned <code>MBeanServerConnection</code>, calling the method causes the
     * corresponding method to be called in the remote MBean server on behalf of the given delegation subject instead of
     * the authenticated subject. The value returned by the MBean server method is the value returned to the client. If
     * the MBean server method produces an <code>Exception</code>, the same <code>Exception</code> is seen by the
     * client. If the MBean server method, or the attempt to call it, produces an <code>Error</code>, the
     * <code>Error</code> is wrapped in a {@link JMXServerErrorException}, which is seen by the client.
     * </p>
     *
     * @param delegationSubject the <code>Subject</code> on behalf of which requests will be performed. Can be null, in which case
     *                          requests will be performed on behalf of the authenticated Subject, if any.
     * @return an object that implements the <code>MBeanServerConnection</code> interface by forwarding its methods to
     *         the remote MBean server on behalf of a given delegation subject.
     */
    public MBeanServerConnection getMBeanServerConnection(Subject delegationSubject) {
        throw new UnsupportedOperationException();
    }

    /**
     * <p>
     * Closes the client connection to its server. Any ongoing or new request using the MBeanServerConnection returned
     * by {@link #getMBeanServerConnection()} will get an <code>IOException</code>.
     * </p>
     * <p/>
     * <p>
     * If <code>close</code> has already been called successfully on this object, calling it again has no effect. If
     * <code>close</code> has never been called, or if it was called but produced an exception, an attempt will be
     * made to close the connection. This attempt can succeed, in which case <code>close</code> will return normally,
     * or it can generate an exception.
     * </p>
     * <p/>
     * <p>
     * Closing a connection is a potentially slow operation. For example, if the server has crashed, the close operation
     * might have to wait for a network protocol timeout. Callers that do not want to block in a close operation should
     * do it in a separate thread.
     * </p>
     *
     * @throws IOException if the connection cannot be closed cleanly. If this exception is thrown, it is not known whether
     *                     the server end of the connection has been cleanly closed.
     */
    public void close() throws IOException {
        if (connected) {
            connected = false;
            try {
                sendConnectionNotificationClosed();
                proxy.destroy();
            } catch (Exception e) {
                LOG.error("Failed to destroy proxy: " + e, e);
                throw new IOException(e.getMessage());
            }
        }
    }

    /**
     * <p>
     * Adds a listener to be informed of changes in connection status. The listener will receive notifications of type
     * {@link JMXConnectionNotification}. An implementation can send other types of notifications too.
     * </p>
     * <p/>
     * <p>
     * Any number of listeners can be added with this method. The same listener can be added more than once with the
     * same or different values for the filter and handback. There is no special treatment of a duplicate entry. For
     * example, if a listener is registered twice with no filter, then its <code>handleNotification</code> method will
     * be called twice for each notification.
     * </p>
     *
     * @param listener a listener to receive connection status notifications.
     * @param filter   a filter to select which notifications are to be delivered to the listener, or null if all
     *                 notifications are to be delivered.
     * @param handback an object to be given to the listener along with each notification. Can be null.
     * @throws NullPointerException if <code>listener</code> is null.
     * @see #removeConnectionNotificationListener
     * @see NotificationBroadcaster#addNotificationListener
     */
    public void addConnectionNotificationListener(NotificationListener listener, NotificationFilter filter,
                                                  Object handback) {
        connectionNotifier.addNotificationListener(listener, filter, handback);
    }

    /**
     * <p>
     * Removes a listener from the list to be informed of changes in status. The listener must previously have been
     * added. If there is more than one matching listener, all are removed.
     * </p>
     *
     * @param listener a listener to receive connection status notifications.
     * @throws NullPointerException      if <code>listener</code> is null.
     * @throws ListenerNotFoundException if the listener is not registered with this <code>JMXConnector</code>.
     * @see #removeConnectionNotificationListener(NotificationListener, NotificationFilter, Object)
     * @see #addConnectionNotificationListener
     * @see NotificationEmitter#removeNotificationListener
     */
    public void removeConnectionNotificationListener(NotificationListener listener) throws ListenerNotFoundException {
        connectionNotifier.removeNotificationListener(listener);
    }

    /**
     * <p>
     * Removes a listener from the list to be informed of changes in status. The listener must previously have been
     * added with the same three parameters. If there is more than one matching listener, only one is removed.
     * </p>
     *
     * @param l        a listener to receive connection status notifications.
     * @param f        a filter to select which notifications are to be delivered to the listener. Can be null.
     * @param handback an object to be given to the listener along with each notification. Can be null.
     * @throws ListenerNotFoundException if the listener is not registered with this <code>JMXConnector</code>, or is not registered
     *                                   with the given filter and handback.
     * @see #removeConnectionNotificationListener(NotificationListener)
     * @see #addConnectionNotificationListener
     * @see NotificationEmitter#removeNotificationListener
     */
    public void removeConnectionNotificationListener(NotificationListener l, NotificationFilter f, Object handback)
        throws ListenerNotFoundException {
        connectionNotifier.removeNotificationListener(l, f, handback);
    }


    /**
     * <p>
     * Gets this connection's ID from the connector server. For a given connector server, every connection will have a
     * unique id which does not change during the lifetime of the connection.
     * </p>
     *
     * @return the unique ID of this connection. This is the same as the ID that the connector server includes in its
     *         {@link JMXConnectionNotification}s. The {@link javax.management.remote package description} describes the
     *         conventions for connection IDs.
     */
    public String getConnectionId() {
        return connectionId;
    }

    public CamelContext getCamelContext() {
        if (camelContext == null) {
            LOG.warn("No CamelContext injected so creating a default implementation");
            // TODO should we barf or create a default one?
            camelContext = new DefaultCamelContext();
        }
        return camelContext;
    }

    public void setCamelContext(CamelContext camelContext) {
        this.camelContext = camelContext;
    }

    private void sendConnectionNotificationOpened() {
        JMXConnectionNotification notification = new JMXConnectionNotification(JMXConnectionNotification.OPENED, this,
                getConnectionId(), notificationNumber.incrementAndGet(), "Connection opened", null);
        connectionNotifier.sendNotification(notification);
    }

    private void sendConnectionNotificationClosed() {
        JMXConnectionNotification notification = new JMXConnectionNotification(JMXConnectionNotification.CLOSED, this,
                getConnectionId(), notificationNumber.incrementAndGet(), "Connection closed", null);
        connectionNotifier.sendNotification(notification);
    }

    private void sendConnectionNotificationFailed(String message) {
        JMXConnectionNotification notification = new JMXConnectionNotification(JMXConnectionNotification.FAILED, this,
                getConnectionId(), notificationNumber.incrementAndGet(), message, null);
        connectionNotifier.sendNotification(notification);
    }
}
