/*
 * 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.brooklyn.feed.jmx;

import static com.google.common.base.Preconditions.checkNotNull;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.feed.AbstractFeed;
import org.apache.brooklyn.core.feed.AttributePollHandler;
import org.apache.brooklyn.core.feed.DelegatingPollHandler;
import org.apache.brooklyn.core.feed.PollHandler;
import org.apache.brooklyn.core.feed.Poller;
import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.reflect.TypeToken;


/**
 * Provides a feed of attribute values, by polling or subscribing over jmx.
 * 
 * Example usage (e.g. in an entity that extends {@link SoftwareProcessImpl}):
 * <pre>
 * {@code
 * private JmxFeed feed;
 * 
 * //@Override
 * protected void connectSensors() {
 *   super.connectSensors();
 *   
 *   feed = JmxFeed.builder()
 *       .entity(this)
 *       .period(500, TimeUnit.MILLISECONDS)
 *       .pollAttribute(new JmxAttributePollConfig<Integer>(ERROR_COUNT)
 *           .objectName(requestProcessorMbeanName)
 *           .attributeName("errorCount"))
 *       .pollAttribute(new JmxAttributePollConfig<Boolean>(SERVICE_UP)
 *           .objectName(serverMbeanName)
 *           .attributeName("Started")
 *           .onError(Functions.constant(false)))
 *       .build();
 * }
 * 
 * {@literal @}Override
 * protected void disconnectSensors() {
 *   super.disconnectSensors();
 *   if (feed != null) feed.stop();
 * }
 * }
 * </pre>
 * 
 * @author aled
 */
public class JmxFeed extends AbstractFeed {

    public static final Logger log = LoggerFactory.getLogger(JmxFeed.class);

    public static final long JMX_CONNECTION_TIMEOUT_MS = 120*1000;

    public static final ConfigKey<JmxHelper> HELPER = ConfigKeys.newConfigKey(JmxHelper.class, "helper");
    public static final ConfigKey<Boolean> OWN_HELPER = ConfigKeys.newBooleanConfigKey("ownHelper");
    public static final ConfigKey<String> JMX_URI = ConfigKeys.newStringConfigKey("jmxUri");
    public static final ConfigKey<Long> JMX_CONNECTION_TIMEOUT = ConfigKeys.newLongConfigKey("jmxConnectionTimeout");
    
    @SuppressWarnings("serial")
    public static final ConfigKey<SetMultimap<String, JmxAttributePollConfig<?>>> ATTRIBUTE_POLLS = ConfigKeys.newConfigKey(
            new TypeToken<SetMultimap<String, JmxAttributePollConfig<?>>>() {},
            "attributePolls");

    @SuppressWarnings("serial")
    public static final ConfigKey<SetMultimap<List<?>, JmxOperationPollConfig<?>>> OPERATION_POLLS = ConfigKeys.newConfigKey(
            new TypeToken<SetMultimap<List<?>, JmxOperationPollConfig<?>>>() {},
            "operationPolls");

    @SuppressWarnings("serial")
    public static final ConfigKey<SetMultimap<NotificationFilter, JmxNotificationSubscriptionConfig<?>>> NOTIFICATION_SUBSCRIPTIONS = ConfigKeys.newConfigKey(
            new TypeToken<SetMultimap<NotificationFilter, JmxNotificationSubscriptionConfig<?>>>() {},
            "notificationPolls");

    public static Builder builder() {
        return new Builder();
    }
    
    public static class Builder {
        private Entity entity;
        private JmxHelper helper;
        private long jmxConnectionTimeout = JMX_CONNECTION_TIMEOUT_MS;
        private long period = 500;
        private TimeUnit periodUnits = TimeUnit.MILLISECONDS;
        private List<JmxAttributePollConfig<?>> attributePolls = Lists.newArrayList();
        private List<JmxOperationPollConfig<?>> operationPolls = Lists.newArrayList();
        private List<JmxNotificationSubscriptionConfig<?>> notificationSubscriptions = Lists.newArrayList();
        private String uniqueTag;
        private volatile boolean built;
        
        public Builder entity(Entity val) {
            this.entity = val;
            return this;
        }
        public Builder helper(JmxHelper val) {
            this.helper = val;
            return this;
        }
        public Builder period(Duration duration) {
            return period(duration.toMilliseconds(), TimeUnit.MILLISECONDS);
        }
        public Builder period(long millis) {
            return period(millis, TimeUnit.MILLISECONDS);
        }
        public Builder period(long val, TimeUnit units) {
            this.period = val;
            this.periodUnits = units;
            return this;
        }
        public Builder pollAttribute(JmxAttributePollConfig<?> config) {
            attributePolls.add(config);
            return this;
        }
        public Builder pollOperation(JmxOperationPollConfig<?> config) {
            operationPolls.add(config);
            return this;
        }
        public Builder subscribeToNotification(JmxNotificationSubscriptionConfig<?> config) {
            notificationSubscriptions.add(config);
            return this;
        }
        public Builder uniqueTag(String uniqueTag) {
            this.uniqueTag = uniqueTag;
            return this;
        }
        public JmxFeed build() {
            built = true;
            JmxFeed result = new JmxFeed(this);
            result.setEntity(checkNotNull((EntityLocal)entity, "entity"));
            result.start();
            return result;
        }
        @Override
        protected void finalize() {
            if (!built) log.warn("JmxFeed.Builder created, but build() never called");
        }
    }

    private final SetMultimap<ObjectName, NotificationListener> notificationListeners = HashMultimap.create();

    /**
     * For rebind; do not call directly; use builder
     */
    public JmxFeed() {
    }

    protected JmxFeed(Builder builder) {
        super();
        if (builder.helper != null) {
            JmxHelper helper = builder.helper;
            setConfig(HELPER, helper);
            setConfig(OWN_HELPER, false);
            setConfig(JMX_URI, helper.getUrl());
        }
        setConfig(JMX_CONNECTION_TIMEOUT, builder.jmxConnectionTimeout);
        
        SetMultimap<String, JmxAttributePollConfig<?>> attributePolls = HashMultimap.<String,JmxAttributePollConfig<?>>create();
        for (JmxAttributePollConfig<?> config : builder.attributePolls) {
            if (!config.isEnabled()) continue;
            @SuppressWarnings({ "rawtypes", "unchecked" })
            JmxAttributePollConfig<?> configCopy = new JmxAttributePollConfig(config);
            if (configCopy.getPeriod() < 0) configCopy.period(builder.period, builder.periodUnits);
            attributePolls.put(configCopy.getObjectName().getCanonicalName() + configCopy.getAttributeName(), configCopy);
        }
        setConfig(ATTRIBUTE_POLLS, attributePolls);
        
        SetMultimap<List<?>, JmxOperationPollConfig<?>> operationPolls = HashMultimap.<List<?>,JmxOperationPollConfig<?>>create();
        for (JmxOperationPollConfig<?> config : builder.operationPolls) {
            if (!config.isEnabled()) continue;
            @SuppressWarnings({ "rawtypes", "unchecked" })
            JmxOperationPollConfig<?> configCopy = new JmxOperationPollConfig(config);
            if (configCopy.getPeriod() < 0) configCopy.period(builder.period, builder.periodUnits);
            operationPolls.put(configCopy.buildOperationIdentity(), configCopy);
        }
        setConfig(OPERATION_POLLS, operationPolls);
        
        SetMultimap<NotificationFilter, JmxNotificationSubscriptionConfig<?>> notificationSubscriptions = HashMultimap.create();
        for (JmxNotificationSubscriptionConfig<?> config : builder.notificationSubscriptions) {
            if (!config.isEnabled()) continue;
            notificationSubscriptions.put(config.getNotificationFilter(), config);
        }
        setConfig(NOTIFICATION_SUBSCRIPTIONS, notificationSubscriptions);
        initUniqueTag(builder.uniqueTag, attributePolls, operationPolls, notificationSubscriptions);
    }

    @Override
    public void setEntity(EntityLocal entity) {
        if (getConfig(HELPER) == null) {
            JmxHelper helper = new JmxHelper(entity);
            setConfig(HELPER, helper);
            setConfig(OWN_HELPER, true);
            setConfig(JMX_URI, helper.getUrl());
        }
        super.setEntity(entity);
    }
    
    public String getJmxUri() {
        return getConfig(JMX_URI);
    }
    
    protected JmxHelper getHelper() {
        return getConfig(HELPER);
    }
    
    @Override
    @SuppressWarnings("unchecked")
    protected Poller<Object> getPoller() {
        return (Poller<Object>) super.getPoller();
    }
    
    @Override
    protected boolean isConnected() {
        return super.isConnected() && getHelper().isConnected();
    }
    
    @Override
    protected void preStart() {
        /*
         * All actions on the JmxHelper are done async (through the poller's threading) so we don't 
         * block on start/rebind if the entity is unreachable 
         * (without this we get a 120s pause in JmxHelper.connect restarting)
         */
        final SetMultimap<NotificationFilter, JmxNotificationSubscriptionConfig<?>> notificationSubscriptions = getConfig(NOTIFICATION_SUBSCRIPTIONS);
        final SetMultimap<List<?>, JmxOperationPollConfig<?>> operationPolls = getConfig(OPERATION_POLLS);
        final SetMultimap<String, JmxAttributePollConfig<?>> attributePolls = getConfig(ATTRIBUTE_POLLS);
        
        getPoller().submit(new Callable<Void>() {
               @Override public Void call() {
                   getHelper().connect(getConfig(JMX_CONNECTION_TIMEOUT));
                   return null;
               }
               @Override public String toString() { return "Connect JMX "+getHelper().getUrl(); }
           });
        
        for (final NotificationFilter filter : notificationSubscriptions.keySet()) {
            getPoller().submit(new Callable<Void>() {
                @Override public Void call() {
                    // TODO Could config.getObjectName have wildcards? Is this code safe?
                    Set<JmxNotificationSubscriptionConfig<?>> configs = notificationSubscriptions.get(filter);
                    NotificationListener listener = registerNotificationListener(configs);
                    ObjectName objectName = Iterables.get(configs, 0).getObjectName();
                    notificationListeners.put(objectName, listener);
                    return null;
                }
                @Override public String toString() { return "Register JMX notifications: "+notificationSubscriptions.get(filter); }
            });
        }
        
        // Setup polling of sensors
        for (final String jmxAttributeName : attributePolls.keys()) {
            registerAttributePoller(attributePolls.get(jmxAttributeName));
        }
        
        // Setup polling of operations
        for (final List<?> operationIdentifier : operationPolls.keys()) {
            registerOperationPoller(operationPolls.get(operationIdentifier));
        }
    }
    
    @Override
    protected void preStop() {
        super.preStop();

        for (Map.Entry<ObjectName, NotificationListener> entry : notificationListeners.entries()) {
            unregisterNotificationListener(entry.getKey(), entry.getValue());
        }
        notificationListeners.clear();
    }
    
    @Override
    protected void postStop() {
        super.postStop();
        JmxHelper helper = getHelper();
        Boolean ownHelper = getConfig(OWN_HELPER);
        if (helper != null && ownHelper) helper.terminate();
    }
    
    /**
     * Registers to poll a jmx-operation for an ObjectName, where all the given configs are for the same ObjectName + operation + parameters.
     */
    private void registerOperationPoller(Set<JmxOperationPollConfig<?>> configs) {
        Set<AttributePollHandler<? super Object>> handlers = Sets.newLinkedHashSet();
        long minPeriod = Integer.MAX_VALUE;
        
        final ObjectName objectName = Iterables.get(configs, 0).getObjectName();
        final String operationName = Iterables.get(configs, 0).getOperationName();
        final List<String> signature = Iterables.get(configs, 0).getSignature();
        final List<?> params = Iterables.get(configs, 0).getParams();
        
        for (JmxOperationPollConfig<?> config : configs) {
            handlers.add(new AttributePollHandler<Object>(config, getEntity(), this));
            if (config.getPeriod() > 0) minPeriod = Math.min(minPeriod, config.getPeriod());
        }
        
        getPoller().scheduleAtFixedRate(
                new Callable<Object>() {
                    @Override
                    public Object call() throws Exception {
                        if (log.isDebugEnabled()) log.debug("jmx operation polling for {} sensors at {} -> {}", new Object[] {getEntity(), getJmxUri(), operationName});
                        if (signature.size() == params.size()) {
                            return getHelper().operation(objectName, operationName, signature, params);
                        } else {
                            return getHelper().operation(objectName, operationName, params.toArray());
                        }
                    }
                }, 
                new DelegatingPollHandler<Object>(handlers), minPeriod);
    }

    /**
     * Registers to poll a jmx-attribute for an ObjectName, where all the given configs are for that same ObjectName + attribute.
     */
    private void registerAttributePoller(Set<JmxAttributePollConfig<?>> configs) {
        Set<AttributePollHandler<? super Object>> handlers = Sets.newLinkedHashSet();
        long minPeriod = Integer.MAX_VALUE;
        
        final ObjectName objectName = Iterables.get(configs, 0).getObjectName();
        final String jmxAttributeName = Iterables.get(configs, 0).getAttributeName();
        
        for (JmxAttributePollConfig<?> config : configs) {
            handlers.add(new AttributePollHandler<Object>(config, getEntity(), this));
            if (config.getPeriod() > 0) minPeriod = Math.min(minPeriod, config.getPeriod());
        }
        
        // TODO Not good calling this holding the synchronization lock
        getPoller().scheduleAtFixedRate(
                new Callable<Object>() {
                    @Override
                    public Object call() throws Exception {
                        if (log.isTraceEnabled()) log.trace("jmx attribute polling for {} sensors at {} -> {}", new Object[] {getEntity(), getJmxUri(), jmxAttributeName});
                        return getHelper().getAttribute(objectName, jmxAttributeName);
                    }
                }, 
                new DelegatingPollHandler<Object>(handlers), minPeriod);
    }

    /**
     * Registers to subscribe to notifications for an ObjectName, where all the given configs are for that same ObjectName + filter.
     */
    private NotificationListener registerNotificationListener(Set<JmxNotificationSubscriptionConfig<?>> configs) {
        final List<AttributePollHandler<? super javax.management.Notification>> handlers = Lists.newArrayList();

        final ObjectName objectName = Iterables.get(configs, 0).getObjectName();
        final NotificationFilter filter = Iterables.get(configs, 0).getNotificationFilter();

        for (final JmxNotificationSubscriptionConfig<?> config : configs) {
            AttributePollHandler<javax.management.Notification> handler = new AttributePollHandler<javax.management.Notification>(config, getEntity(), this) {
                @Override protected Object transformValueOnSuccess(javax.management.Notification val) {
                    if (config.getOnNotification() != null) {
                        return config.getOnNotification().apply(val);
                    } else {
                        Object result = super.transformValueOnSuccess(val);
                        if (result instanceof javax.management.Notification)
                            return ((javax.management.Notification)result).getUserData();
                        return result;
                    }
                }
            };
            handlers.add(handler);
        }
        final PollHandler<javax.management.Notification> compoundHandler = new DelegatingPollHandler<javax.management.Notification>(handlers);
        
        NotificationListener listener = new NotificationListener() {
            @Override public void handleNotification(Notification notification, Object handback) {
                compoundHandler.onSuccess(notification);
            }
        };
        getHelper().addNotificationListener(objectName, listener, filter);
        
        return listener;
    }
    
    private void unregisterNotificationListener(ObjectName objectName, NotificationListener listener) {
        try {
            getHelper().removeNotificationListener(objectName, listener);
        } catch (RuntimeException e) {
            log.warn("Failed to unregister listener: "+objectName+", "+listener+"; continuing...", e);
        }
    }
    
    @Override
    public String toString() {
        return "JmxFeed["+(getManagementContext()!=null&&getManagementContext().isRunning()?getJmxUri():"mgmt-not-running")+"]";
    }
}
