/*
 *  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.tracer.dynamic.jmx;

import org.graalvm.visualvm.application.Application;
import org.graalvm.visualvm.tools.jmx.JmxModel;
import org.graalvm.visualvm.tools.jmx.JmxModelFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.RuntimeMBeanException;

/**
 * A per application cached MBean attribute values provider<br/>
 * It uses a timestamp to decide whether fresh values should be retrieved
 * from the server. It also tries to batch the retrieval process to reduce
 * the network utilization overhead.
 *
 * @author Jaroslav Bachorik
 */
final public class JMXValueCache {
    final private static Map<Application, JMXValueCache> instanceMap = new WeakHashMap<Application, JMXValueCache>();
    final private Map<ObjectName, Collection<String>> attributeMap = new HashMap<ObjectName, Collection<String>>();
    final private Map<String, Object> valueMap = new HashMap<String, Object>();

    final private MBeanServerConnection connection;
    volatile private long lastTimestamp = 0L;

    private JMXValueCache(Application app) {
        JmxModel model = JmxModelFactory.getJmxModelFor(app);
        connection = model.getMBeanServerConnection();
        
    }

    public static JMXValueCache forApplication(Application app) {
        synchronized(instanceMap) {
            JMXValueCache cvp = instanceMap.get(app);
            if (cvp == null) {
                cvp = new JMXValueCache(app);
                instanceMap.put(app, cvp);
            }
            return cvp;
        }
    }

    public JMXValueCache register(ObjectName name, String attribute) {
        register(name, Collections.singleton(attribute));
        return this;
    }

    public JMXValueCache register(ObjectName name, Collection<String> attributes) {
        synchronized(attributeMap) {
            Collection<String> existingAttribs = attributeMap.get(name);
            if (existingAttribs == null) {
                existingAttribs = new ArrayList<String>();
                attributeMap.put(name, existingAttribs);
            }
            existingAttribs.addAll(attributes);
            lastTimestamp = -1; // need to clear the timestamp so the cache is loaded at the next getValue() request
        }
        return this;
    }

    public JMXValueCache unregister(ObjectName name, String attribute) {
        unregister(name, Collections.singleton(attribute));
        return this;
    }

    public JMXValueCache unregister(ObjectName name, Collection<String> attributes) {
        synchronized(attributeMap) {
            Collection<String> existingAttribs = attributeMap.get(name);
            if (existingAttribs != null) {
                existingAttribs.removeAll(attributes);
            }
        }
        return this;
    }

    public MBeanAttributeInfo getInfo(ObjectName on, String attribute) {
        try {
            MBeanInfo mbeanInfo = connection.getMBeanInfo(on);
            for (MBeanAttributeInfo mbAttrInfo : mbeanInfo.getAttributes()) {
                if (mbAttrInfo.getName().equals(attribute)) {
                    return mbAttrInfo;
                }
            }
        } catch (Exception e) {
        }
        return null;
    }

    public Object getValue(ObjectName name, String attribute, long timestamp) {
        refreshCache(timestamp);
        return valueMap.get(getId(name, attribute));
    }
    
    private void refreshCache(long timestamp) {
        if (lastTimestamp == timestamp) {
            return;
        }

        lastTimestamp = timestamp;

        synchronized(attributeMap) {
            for(Map.Entry<ObjectName, Collection<String>> entry : attributeMap.entrySet()) {
                try {
                    AttributeList al = connection.getAttributes(entry.getKey(), entry.getValue().toArray(new String[entry.getValue().size()]));
                    for(Attribute a : al.asList()) {
                        valueMap.put(getId(entry.getKey(), a.getName()), a.getValue());
                    }
                    continue;
                } catch (RuntimeMBeanException ex) {
                } catch (ReflectionException ex) {
                } catch (IOException ex) {
                } catch (InstanceNotFoundException e) {
                }
                for(String an : entry.getValue()) {
                    String id = getId(entry.getKey(), an);
                    if (!valueMap.containsKey(id)) valueMap.put(id, 0);
                }
            }
        }
    }

    private String getId(ObjectName on, String attrName) {
        return on.toString() + "#" + attrName;
    }
}
