/*
 * RED5 Open Source Media Server - https://github.com/Red5/ Copyright 2006-2016 by respective authors (see below). All rights reserved. Licensed 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.red5.server;

import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.StandardMBean;

import org.red5.logging.Red5LoggerFactory;
import org.red5.server.jmx.mxbeans.ContextLoaderMXBean;
import org.slf4j.Logger;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.web.context.support.XmlWebApplicationContext;

/**
 * Red5 applications loader
 * 
 * @author The Red5 Project
 * @author Tiago Jacobs ([email protected])
 * @author Paul Gregoire ([email protected])
 */
@ManagedResource(objectName = "org.red5.server:name=contextLoader,type=ContextLoader", description = "ContextLoader")
public class ContextLoader implements ApplicationContextAware, InitializingBean, DisposableBean, ContextLoaderMXBean {

    protected static Logger log = Red5LoggerFactory.getLogger(ContextLoader.class);

    /**
     * Spring Application context
     */
    protected ApplicationContext applicationContext;

    /**
     * Spring parent app context
     */
    protected ApplicationContext parentContext;

    /**
     * Context location files
     */
    protected String contextsConfig;

    /**
     * MBean object name used for de/registration purposes.
     */
    private ObjectName oName;

    /**
     * Context map
     */
    protected ConcurrentMap<String, ApplicationContext> contextMap;

    /**
     * Registers with JMX and registers a shutdown hook.
     * 
     * @throws Exception
     *             I/O exception, casting exception and others
     */
    public void afterPropertiesSet() throws Exception {
        log.info("ContextLoader init");
        // register in jmx
        registerJMX();
        // initialize
        init();
    }

    /**
     * Un-loads or un-initializes the contexts; this is a shutdown method for this loader.
     */
    public void destroy() throws Exception {
        log.info("ContextLoader un-init");
        shutdown();
    }

    /**
     * Loads context settings from ResourceBundle (.properties file)
     */
    public void init() throws IOException {
        // Load properties bundle
        Properties props = new Properties();
        Resource res = applicationContext.getResource(contextsConfig);
        if (res.exists()) {
            // Load properties file
            props.load(res.getInputStream());
            // Pattern for arbitrary property substitution
            Pattern patt = Pattern.compile("\\$\\{([^\\}]+)\\}");
            Matcher matcher = null;
            // Iterate thru properties keys and replace config attributes with
            // system attributes
            for (Object key : props.keySet()) {
                String name = (String) key;
                String config = props.getProperty(name);
                String configReplaced = config + "";
                //
                matcher = patt.matcher(config);
                //execute the regex
                while (matcher.find()) {
                    String sysProp = matcher.group(1);
                    String systemPropValue = System.getProperty(sysProp);
                    if (systemPropValue == null) {
                        systemPropValue = "null";
                    }
                    configReplaced = configReplaced.replace(String.format("${%s}", sysProp), systemPropValue);
                }
                log.info("Loading: {} = {} => {}", new Object[] { name, config, configReplaced });
                matcher.reset();
                // Load context
                loadContext(name, configReplaced);
            }
            patt = null;
            matcher = null;
        } else {
            log.error("Contexts config must be set");
        }
    }

    /**
     * Loads a context (Red5 application) and stores it in a context map, then adds it's beans to parent (that is, Red5)
     * 
     * @param name
     *            Context name
     * @param config
     *            Filename
     */
    public void loadContext(String name, String config) {
        log.debug("Load context - name: {} config: {}", name, config);
        // check the existence of the config file
        try {
            File configFile = new File(config);
            if (!configFile.exists()) {
                log.warn("Config file was not found at: {}", configFile.getCanonicalPath());
                configFile = new File("file://" + config);
                if (!configFile.exists()) {
                    log.warn("Config file was not found at either: {}", configFile.getCanonicalPath());
                } else {
                    config = "file://" + config;
                }
            }
        } catch (IOException e) {
            log.error("Error looking for config file", e);
        }
        // add the context to the parent, this will be red5.xml
        ConfigurableBeanFactory factory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
        if (factory.containsSingleton(name)) {
            log.warn("Singleton {} already exists, try unload first", name);
            return;
        }
        // if parent context was not set then lookup red5.common
        if (parentContext == null) {
            log.debug("Lookup common - bean:{} local:{} singleton:{}", new Object[] { factory.containsBean("red5.common"), factory.containsLocalBean("red5.common"), factory.containsSingleton("red5.common"), });
            parentContext = (ApplicationContext) factory.getBean("red5.common");
        }
        if (config.startsWith("/")) {
            // Spring always interprets files as relative, so will strip a leading slash unless we tell
            // it otherwise. It also appears to not need this for Windows
            // absolute paths (e.g. C:\Foo\Bar) so we don't catch that either
            String newConfig = "file://" + config;
            log.debug("Resetting {} to {}", config, newConfig);
            config = newConfig;
        }
        ApplicationContext context = new FileSystemXmlApplicationContext(new String[] { config }, parentContext);
        log.debug("Adding to context map - name: {} context: {}", name, context);
        if (contextMap == null) {
            contextMap = new ConcurrentHashMap<>(3, 0.9f, 1);
        }
        contextMap.put(name, context);
        // Register context in parent bean factory
        log.debug("Registering - name: {}", name);
        factory.registerSingleton(name, context);
    }

    /**
     * Unloads a context (Red5 application) and removes it from the context map, then removes it's beans from the parent (that is, Red5)
     * 
     * @param name
     *            Context name
     */
    public void unloadContext(String name) {
        log.debug("Un-load context - name: {}", name);
        ApplicationContext context = contextMap.remove(name);
        log.debug("Context from map: {}", context);
        String[] bnames = BeanFactoryUtils.beanNamesIncludingAncestors(context);
        for (String bname : bnames) {
            log.debug("Bean: {}", bname);
        }
        ConfigurableBeanFactory factory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
        if (factory.containsSingleton(name)) {
            log.debug("Context found in parent, destroying: {}", name);
            FileSystemXmlApplicationContext ctx = (FileSystemXmlApplicationContext) factory.getSingleton(name);
            if (ctx.isRunning()) {
                log.debug("Context was running, attempting to stop");
                ctx.stop();
            }
            if (ctx.isActive()) {
                log.debug("Context is active, attempting to close");
                ctx.close();
            } else {
                try {
                    factory.destroyBean(name, ctx);
                } catch (Exception e) {
                    log.warn("Context destroy failed for: {}", name, e);
                } finally {
                    if (factory.containsSingleton(name)) {
                        log.debug("Singleton still exists, trying another destroy method");
                        ((DefaultListableBeanFactory) factory).destroySingleton(name);
                    }
                }
            }
        } else {
            log.debug("Context does not contain singleton: {}", name);
        }
        context = null;
    }

    /**
     * Shut server down.
     */
    public void shutdown() {
        log.info("Shutting down");
        if (contextMap != null) {
            log.debug("Context map: {}", contextMap);
            try {
                // unload all the contexts in the map
                for (Map.Entry<String, ApplicationContext> entry : contextMap.entrySet()) {
                    String contextName = entry.getKey();
                    log.info("Unloading context {} on shutdown", contextName);
                    unloadContext(contextName);
                }
                contextMap.clear();
            } catch (Exception e) {
                log.warn("Exception shutting down contexts", e);
            } finally {
                contextMap = null;
            }
        }
        unregisterJMX();
        log.info("Shutdown complete");
    }

    /**
     * Return context by name
     * 
     * @param name
     *            Context name
     * @return Application context for given name
     */
    public ApplicationContext getContext(String name) {
        if (contextMap != null) {
            return contextMap.get(name);
        } else {
            return null;
        }
    }

    /**
     * Sets a parent context for child context based on a given key.
     * 
     * @param parentContextKey
     *            key for the parent context
     * @param appContextId
     *            id of the child context
     */
    public void setParentContext(String parentContextKey, String appContextId) {
        log.debug("Set parent context {} on {}", parentContextKey, appContextId);
        ApplicationContext parentContext = getContext(parentContextKey);
        if (parentContext != null) {
            XmlWebApplicationContext childContext = (XmlWebApplicationContext) getContext(appContextId);
            if (childContext != null) {
                childContext.setParent(parentContext);
            } else {
                log.debug("Child context not found");
            }
        } else {
            log.debug("Parent context not found");
        }
    }

    protected void registerJMX() {
        // register with jmx
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        try {
            oName = new ObjectName("org.red5.server:name=contextLoader,type=ContextLoader");
            // check for existing registration before registering
            if (!mbs.isRegistered(oName)) {
                mbs.registerMBean(new StandardMBean(this, ContextLoaderMXBean.class, true), oName);
            } else {
                log.debug("ContextLoader is already registered in JMX");
            }
        } catch (Exception e) {
            log.warn("Error on jmx registration", e);
        }
    }

    protected void unregisterJMX() {
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        try {
            mbs.unregisterMBean(oName);
        } catch (Exception e) {
            log.warn("Exception unregistering: {}", oName, e);
        }
        oName = null;
    }

    /**
     * @param applicationContext
     *            Spring application context
     * @throws BeansException
     *             Top level exception for app context (that is, in fact, beans factory)
     */
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * Setter for parent application context
     * 
     * @param parentContext
     *            Parent Spring application context
     */
    public void setParentContext(ApplicationContext parentContext) {
        this.parentContext = parentContext;
    }

    /**
     * Return parent context
     * 
     * @return parent application context
     */
    public ApplicationContext getParentContext() {
        return parentContext;
    }

    /**
     * Setter for context config name
     * 
     * @param contextsConfig
     *            Context config name
     */
    public void setContextsConfig(String contextsConfig) {
        this.contextsConfig = contextsConfig;
    }

    public String getContextsConfig() {
        return contextsConfig;
    }

}