/*
 * 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.tomcat.util.modeler;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.management.DynamicMBean;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.modeler.modules.ModelerSource;
import org.apache.tomcat.util.res.StringManager;

/**
 * Registry for modeler MBeans. This is the main entry point into modeler. It provides methods to create and manipulate
 * model mbeans and simplify their use. This class is itself a mbean.
 */
public class Registry implements RegistryMBean, MBeanRegistration {

    /**
     * The Log instance to which we will write our log messages.
     */
    private static final Log log = LogFactory.getLog(Registry.class);
    private static final StringManager sm = StringManager.getManager(Registry.class);

    // Support for the factory methods

    /**
     * The registry instance created by our factory method the first time it is called.
     */
    private static Registry registry = null;

    // Per registry fields

    /**
     * The <code>MBeanServer</code> instance that we will use to register management beans.
     */
    private volatile MBeanServer server = null;
    private final Object serverLock = new Object();

    /**
     * The set of ManagedBean instances for the beans this registry knows about, keyed by name.
     */
    private Map<String,ManagedBean> descriptors = new HashMap<>();

    /**
     * List of managed beans, keyed by class name
     */
    private Map<String,ManagedBean> descriptorsByClass = new HashMap<>();

    // map to avoid duplicated searching or loading descriptors
    private Map<String,URL> searchedPaths = new HashMap<>();

    private Object guard;

    // Id - small ints to use array access. No reset on stop()
    // Used for notifications
    private final Hashtable<String,Hashtable<String,Integer>> idDomains = new Hashtable<>();
    private final Hashtable<String,int[]> ids = new Hashtable<>();


    // ----------------------------------------------------------- Constructors

    /**
     * Protected constructor for Registry. Use {@link #getRegistry(Object)} to obtain an instance.
     */
    protected Registry() {
        super();
    }


    // -------------------- Static methods --------------------
    // Factories

    /**
     * Factory method to create (if necessary) and return our <code>Registry</code> instance.
     *
     * @param key   Unused
     * @param guard Prevent access to the registry by untrusted components
     *
     * @return the registry
     *
     * @since 1.1
     */
    public static synchronized Registry getRegistry(Object key, Object guard) {
        if (registry == null) {
            registry = new Registry();
            registry.guard = guard;
        }
        if (registry.guard != null && registry.guard != guard) {
            return null;
        }
        return registry;
    }


    /**
     * Factory method to create (if necessary) and return our <code>Registry</code> instance.
     *
     * @param guard Prevent access to the registry by untrusted components
     *
     * @return the registry
     *
     * @throws IllegalArgumentException if the guard object does not allow access
     */
    public static synchronized Registry getRegistry(Object guard) {
        if (registry == null) {
            registry = new Registry();
            registry.guard = guard;
        }
        if (registry.guard != null && registry.guard != guard) {
            throw new IllegalArgumentException(sm.getString("registry.cannotAccessRegistry", guard));
        }
        return registry;
    }


    /**
     * Disables the registry by replacing it with a no-op implementation. After calling this method,
     * the registry will not load or process MBean descriptors.
     */
    public static synchronized void disableRegistry() {
        if (registry == null) {
            registry = new NoDescriptorRegistry();
        } else if (!(registry instanceof NoDescriptorRegistry)) {
            log.warn(sm.getString("registry.noDisable"));
        }
    }


    // -------------------- Generic methods --------------------

    /**
     * Lifecycle method - clean up the registry metadata. Called from resetMetadata().
     *
     * @since 1.1
     */
    @Override
    public void stop() {
        descriptorsByClass = new HashMap<>();
        descriptors = new HashMap<>();
        searchedPaths = new HashMap<>();
    }


    /**
     * Register a bean by creating a modeler mbean and adding it to the MBeanServer. If metadata is not loaded, we'll
     * look up and read a file named "mbeans-descriptors.ser" or "mbeans-descriptors.xml" in the same package or parent.
     * If the bean is an instance of DynamicMBean. its metadata will be converted to a model mbean, and we'll wrap it so
     * modeler services are supported. If the metadata is still not found, introspection will be used to extract it
     * automatically. If a mbean is already registered under this name, it'll be first unregistered. If the component
     * implements MBeanRegistration, the methods will be called. If the method has a method "setRegistry" that takes a
     * RegistryMBean as parameter, it'll be called with the current registry.
     *
     * @param bean  Object to be registered
     * @param oname Name used for registration
     * @param type  The type of the mbean, as declared in mbeans-descriptors. If null, the name of the class will be
     *                  used. This can be used as a hint or by subclasses.
     *
     * @throws Exception if a registration error occurred
     *
     * @since 1.1
     */
    @Override
    public void registerComponent(Object bean, String oname, String type) throws Exception {
        registerComponent(bean, new ObjectName(oname), type);
    }


    /**
     * Unregister a component. We'll first check if it is registered, and mask all errors. This is mostly a helper.
     *
     * @param oname Name used for unregistration
     *
     * @since 1.1
     */
    @Override
    public void unregisterComponent(String oname) {
        try {
            unregisterComponent(new ObjectName(oname));
        } catch (MalformedObjectNameException e) {
            log.info(sm.getString("registry.objectNameCreateError"), e);
        }
    }


    /**
     * Invoke an operation on a list of mbeans. Can be used to implement lifecycle operations.
     *
     * @param mbeans    list of ObjectName on which we'll invoke the operations
     * @param operation Name of the operation ( init, start, stop, etc)
     * @param failFirst If false, exceptions will be ignored
     *
     * @throws Exception Error invoking operation
     *
     * @since 1.1
     */
    @Override
    public void invoke(List<ObjectName> mbeans, String operation, boolean failFirst) throws Exception {

        if (mbeans == null) {
            return;
        }
        for (ObjectName current : mbeans) {
            try {
                if (current == null) {
                    continue;
                }
                if (getMethodInfo(current, operation) == null) {
                    continue;
                }
                getMBeanServer().invoke(current, operation, new Object[] {}, new String[] {});

            } catch (Exception e) {
                if (failFirst) {
                    throw e;
                }
                log.info(sm.getString("registry.initError"), e);
            }
        }
    }

    // -------------------- ID registry --------------------

    /**
     * Return an int ID for faster access. Will be used for notifications and for other operations we want to optimize.
     *
     * @param domain Namespace
     * @param name   Type of the notification
     *
     * @return A unique id for the domain:name combination
     *
     * @since 1.1
     */
    @Override
    public synchronized int getId(String domain, String name) {
        if (domain == null) {
            domain = "";
        }
        Hashtable<String,Integer> domainTable = idDomains.computeIfAbsent(domain, k -> new Hashtable<>());
        if (name == null) {
            name = "";
        }
        Integer i = domainTable.get(name);

        if (i != null) {
            return i.intValue();
        }

        int[] id = ids.computeIfAbsent(domain, k -> new int[1]);
        int code = id[0]++;
        domainTable.put(name, Integer.valueOf(code));
        return code;
    }


    // -------------------- Metadata --------------------
    // methods from 1.0

    /**
     * Add a new bean metadata to the set of beans known to this registry. This is used by internal components.
     *
     * @param bean The managed bean to be added
     *
     * @since 1.0
     */
    public void addManagedBean(ManagedBean bean) {
        // XXX Use group + name
        descriptors.put(bean.getName(), bean);
        if (bean.getType() != null) {
            descriptorsByClass.put(bean.getType(), bean);
        }
    }


    /**
     * Find and return the managed bean definition for the specified bean name, if any; otherwise return
     * <code>null</code>.
     *
     * @param name Name of the managed bean to be returned. Since 1.1, both short names or the full name of the class
     *                 can be used.
     *
     * @return the managed bean
     *
     * @since 1.0
     */
    public ManagedBean findManagedBean(String name) {
        // XXX Group ?? Use Group + Type
        ManagedBean mb = descriptors.get(name);
        if (mb == null) {
            mb = descriptorsByClass.get(name);
        }
        return mb;
    }


    // -------------------- Helpers --------------------

    /**
     * Get the type of an attribute of the object, from the metadata.
     *
     * @param oname   The bean name
     * @param attName The attribute name
     *
     * @return null if metadata about the attribute is not found
     *
     * @since 1.1
     */
    public String getType(ObjectName oname, String attName) {
        String type;
        MBeanInfo info;
        try {
            info = getMBeanServer().getMBeanInfo(oname);
        } catch (InstanceNotFoundException e) {
            log.info(sm.getString("registry.noMetadata", oname));
            return null;
        } catch (Exception e) {
            log.warn(sm.getString("registry.noMetadata", oname), e);
            return null;
        }

        MBeanAttributeInfo[] attInfo = info.getAttributes();
        for (MBeanAttributeInfo mBeanAttributeInfo : attInfo) {
            if (attName.equals(mBeanAttributeInfo.getName())) {
                type = mBeanAttributeInfo.getType();
                return type;
            }
        }
        return null;
    }


    /**
     * Find the operation info for a method
     *
     * @param oname  The bean name
     * @param opName The operation name
     *
     * @return the operation info for the specified operation
     */
    public MBeanOperationInfo getMethodInfo(ObjectName oname, String opName) {
        MBeanInfo info;
        try {
            info = getMBeanServer().getMBeanInfo(oname);
        } catch (InstanceNotFoundException e) {
            log.info(sm.getString("registry.noMetadata", oname));
            return null;
        } catch (Exception e) {
            log.warn(sm.getString("registry.noMetadata", oname), e);
            return null;
        }
        MBeanOperationInfo[] attInfo = info.getOperations();
        for (MBeanOperationInfo mBeanOperationInfo : attInfo) {
            if (opName.equals(mBeanOperationInfo.getName())) {
                return mBeanOperationInfo;
            }
        }
        return null;
    }

    /**
     * Find the operation info for a method.
     *
     * @param oname    The bean name
     * @param opName   The operation name
     * @param argCount The number of arguments to the method
     *
     * @return the operation info for the specified operation
     *
     * @throws InstanceNotFoundException If the object name is not bound to an MBean
     */
    public MBeanOperationInfo getMethodInfo(ObjectName oname, String opName, int argCount)
            throws InstanceNotFoundException {
        MBeanInfo info;
        try {
            info = getMBeanServer().getMBeanInfo(oname);
        } catch (InstanceNotFoundException infe) {
            throw infe;
        } catch (Exception e) {
            log.warn(sm.getString("registry.noMetadata", oname), e);
            return null;
        }
        MBeanOperationInfo[] attInfo = info.getOperations();
        for (MBeanOperationInfo mBeanOperationInfo : attInfo) {
            if (opName.equals(mBeanOperationInfo.getName()) && argCount == mBeanOperationInfo.getSignature().length) {
                return mBeanOperationInfo;
            }
        }
        return null;
    }

    /**
     * Unregister a component. This is just a helper that avoids exceptions by checking if the mbean is already
     * registered
     *
     * @param oname The bean name
     */
    public void unregisterComponent(ObjectName oname) {
        try {
            if (oname != null && getMBeanServer().isRegistered(oname)) {
                getMBeanServer().unregisterMBean(oname);
            }
        } catch (Throwable t) {
            log.error(sm.getString("registry.unregisterError"), t);
        }
    }


    /**
     * Factory method to create (if necessary) and return our <code>MBeanServer</code> instance.
     *
     * @return the MBean server
     */
    public MBeanServer getMBeanServer() {
        if (server == null) {
            synchronized (serverLock) {
                if (server == null) {
                    if (!MBeanServerFactory.findMBeanServer(null).isEmpty()) {
                        server = MBeanServerFactory.findMBeanServer(null).get(0);
                        if (log.isDebugEnabled()) {
                            log.debug(sm.getString("registry.existingServer"));
                        }
                    } else {
                        server = ManagementFactory.getPlatformMBeanServer();
                        if (log.isDebugEnabled()) {
                            log.debug(sm.getString("registry.createdServer"));
                        }
                    }
                }
            }
        }
        return server;
    }


    /**
     * Find or load metadata.
     *
     * @param bean      The bean
     * @param beanClass The bean class
     * @param type      The registry type
     *
     * @return the managed bean
     *
     * @throws Exception An error occurred
     */
    public ManagedBean findManagedBean(Object bean, Class<?> beanClass, String type) throws Exception {

        if (bean != null && beanClass == null) {
            beanClass = bean.getClass();
        }

        if (type == null) {
            if (beanClass == null) {
                throw new IllegalArgumentException(sm.getString("registry.invalidArguments"));
            }
            type = beanClass.getName();
        }

        // first look for existing descriptor
        ManagedBean managed = findManagedBean(type);

        // Search for a descriptor in the same package
        if (managed == null) {
            // check package and parent packages
            if (log.isTraceEnabled()) {
                log.trace("Looking for descriptor ");
            }
            findDescriptor(beanClass, type);

            managed = findManagedBean(type);
        }

        // Still not found - use introspection
        if (managed == null) {
            if (log.isTraceEnabled()) {
                log.trace("Introspecting ");
            }

            // introspection
            load("MbeansDescriptorsIntrospectionSource", beanClass, type);

            managed = findManagedBean(type);
            if (managed == null) {
                log.warn(sm.getString("registry.noTypeMetadata", type));
                return null;
            }
            managed.setName(type);
            addManagedBean(managed);
        }
        return managed;
    }


    /**
     * Convert a string to object, based on type. Used by several components. We could provide some pluggability. It is
     * here to keep things consistent and avoid duplication in other tasks
     *
     * @param type  Fully qualified class name of the resulting value
     * @param value String value to be converted
     *
     * @return Converted value
     */
    public Object convertValue(String type, String value) {
        Object objValue = value;

        if (type == null || "java.lang.String".equals(type)) {
            // string is default
        } else if ("javax.management.ObjectName".equals(type) || "ObjectName".equals(type)) {
            try {
                objValue = new ObjectName(value);
            } catch (MalformedObjectNameException e) {
                return null;
            }
        } else if ("java.lang.Integer".equals(type) || "int".equals(type)) {
            objValue = Integer.valueOf(value);
        } else if ("java.lang.Long".equals(type) || "long".equals(type)) {
            objValue = Long.valueOf(value);
        } else if ("java.lang.Boolean".equals(type) || "boolean".equals(type)) {
            objValue = Boolean.valueOf(value);
        }
        return objValue;
    }


    /**
     * Load descriptors.
     *
     * @param sourceType The source type
     * @param source     The bean
     * @param param      A type to load
     *
     * @return List of descriptors
     *
     * @throws Exception Error loading descriptors
     */
    public List<ObjectName> load(String sourceType, Object source, String param) throws Exception {
        if (log.isTraceEnabled()) {
            log.trace("load " + source);
        }
        String location;
        String type;
        Object inputsource;

        if (source instanceof URL) {
            URL url = (URL) source;
            location = url.toString();
            type = param;
            inputsource = url.openStream();
            if (sourceType == null && location.endsWith(".xml")) {
                sourceType = "MbeansDescriptorsDigesterSource";
            }
        } else if (source instanceof File) {
            location = ((File) source).getAbsolutePath();
            inputsource = new FileInputStream((File) source);
            type = param;
            if (sourceType == null && location.endsWith(".xml")) {
                sourceType = "MbeansDescriptorsDigesterSource";
            }
        } else if (source instanceof InputStream) {
            type = param;
            inputsource = source;
        } else if (source instanceof Class<?>) {
            location = ((Class<?>) source).getName();
            type = param;
            inputsource = source;
            if (sourceType == null) {
                sourceType = "MbeansDescriptorsIntrospectionSource";
            }
        } else {
            throw new IllegalArgumentException(sm.getString("registry.invalidSource"));
        }

        if (sourceType == null) {
            sourceType = "MbeansDescriptorsDigesterSource";
        }
        ModelerSource ds = getModelerSource(sourceType);

        return ds.loadDescriptors(this, type, inputsource);
    }


    /**
     * Register a component
     *
     * @param bean  The bean
     * @param oname The object name
     * @param type  The registry type
     *
     * @throws Exception Error registering component
     */
    public void registerComponent(Object bean, ObjectName oname, String type) throws Exception {
        if (log.isTraceEnabled()) {
            log.trace("Managed= " + oname);
        }

        if (bean == null) {
            log.error(sm.getString("registry.nullBean", oname));
            return;
        }

        try {
            if (type == null) {
                type = bean.getClass().getName();
            }

            ManagedBean managed = findManagedBean(null, bean.getClass(), type);

            // The real mbean is created and registered
            DynamicMBean mbean = managed.createMBean(bean);

            if (getMBeanServer().isRegistered(oname)) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("registry.unregisterExisting", oname));
                }
                getMBeanServer().unregisterMBean(oname);
            }

            getMBeanServer().registerMBean(mbean, oname);
        } catch (Exception e) {
            log.error(sm.getString("registry.registerError", oname), e);
            throw e;
        }
    }


    /**
     * Lookup the component descriptor in the package and in the parent packages.
     *
     * @param packageName The package name
     * @param classLoader The class loader
     */
    public void loadDescriptors(String packageName, ClassLoader classLoader) {
        String res = packageName.replace('.', '/');

        if (log.isTraceEnabled()) {
            log.trace("Finding descriptor " + res);
        }

        if (searchedPaths.get(packageName) != null) {
            return;
        }

        String descriptors = res + "/mbeans-descriptors.xml";
        URL dURL = classLoader.getResource(descriptors);

        if (dURL == null) {
            return;
        }

        if (log.isTraceEnabled()) {
            log.trace("Found " + dURL);
        }
        searchedPaths.put(packageName, dURL);
        try {
            load("MbeansDescriptorsDigesterSource", dURL, null);
        } catch (Exception e) {
            log.error(sm.getString("registry.loadError", dURL));
        }
    }


    /**
     * Lookup the component descriptor in the package and in the parent packages.
     */
    private void findDescriptor(Class<?> beanClass, String type) {
        if (type == null) {
            type = beanClass.getName();
        }
        ClassLoader classLoader = null;
        if (beanClass != null) {
            classLoader = beanClass.getClassLoader();
        }
        if (classLoader == null) {
            classLoader = Thread.currentThread().getContextClassLoader();
        }
        if (classLoader == null) {
            classLoader = this.getClass().getClassLoader();
        }

        String pkg = type;
        while (pkg.indexOf('.') > 0) {
            int lastComp = pkg.lastIndexOf('.');
            if (lastComp <= 0) {
                return;
            }
            pkg = pkg.substring(0, lastComp);
            if (searchedPaths.get(pkg) != null) {
                return;
            }
            loadDescriptors(pkg, classLoader);
        }
    }


    private ModelerSource getModelerSource(String type) throws Exception {
        if (type == null) {
            type = "MbeansDescriptorsDigesterSource";
        }
        if (!type.contains(".")) {
            type = "org.apache.tomcat.util.modeler.modules." + type;
        }

        Class<?> c = Class.forName(type);
        return (ModelerSource) c.getConstructor().newInstance();
    }


    // -------------------- Registration --------------------

    @Override
    public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception {
        synchronized (serverLock) {
            this.server = server;
        }
        return name;
    }


    @Override
    public void postRegister(Boolean registrationDone) {
    }


    @Override
    public void preDeregister() throws Exception {
    }


    @Override
    public void postDeregister() {
    }
}
