/*
 * 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.felix.log;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.List;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.wiring.BundleWiring;
import org.osgi.service.log.LogLevel;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;

public class ConfigurationListenerImpl {
    private static final String CONFIGURATION_CLASS = "org.osgi.service.cm.Configuration";
    private static final String CONFIGURATION_ADMIN_CLASS = "org.osgi.service.cm.ConfigurationAdmin";
    private static final String CONFIGURATION_EVENT_CLASS = "org.osgi.service.cm.ConfigurationEvent";
    private static final String CONFIGURATION_LISTENER_CLASS = "org.osgi.service.cm.ConfigurationListener";
    private static final String LOGGER_ADMIN_PID = "org.osgi.service.log.admin";
    private static final String LOGGER_ADMIN_PID_PREFIX = LOGGER_ADMIN_PID + '|';

    /** ConfigurationAdmin tracker */
    final ServiceTracker<?, ?> m_cmtracker;
    final BundleContext m_context;
    final Log m_log;
    final LoggerAdminImpl m_loggerAdmin;

    public ConfigurationListenerImpl(final BundleContext context, final Log log, final LoggerAdminImpl loggerAdmin) throws Exception {
        m_context = context;
        m_log = log;
        m_loggerAdmin = loggerAdmin;

        Filter filter = context.createFilter(String.format("(%s=%s)", Constants.OBJECTCLASS, CONFIGURATION_ADMIN_CLASS));

        m_cmtracker = new ServiceTracker<>(m_context, filter, new CLCustomizer());
        m_cmtracker.open();
    }

    public void close() {
        m_cmtracker.close();
    }

    class CLProxy {
        private final Object m_ca;
        private final ServiceReference<?> m_caReference;
        private final ServiceRegistration<?> m_registration;
        private final Class<?> m_caClass;
        private final Class<?> m_ceClass;
        private final Class<?> m_clClass;
        private final Class<?> m_configurationClass;
        private final Method m_caGetConfiguration;
        private final Method m_caListConfigurations;
        private final Method m_ceGetPid;
        private final Method m_ceGetType;
        private final Method m_clConfigurationEvent;
        private final Method m_configurationGetProperties;
        private final Method m_configurationGetProcessedProperties;

        public CLProxy(ServiceReference<?> caReference) {
            m_caReference = caReference;
            m_ca = m_context.getService(m_caReference);

            final Bundle bundle = m_caReference.getBundle();
            if ( bundle == null )
            {
                throw new IllegalStateException("Service already unregistered again");
            }
            try {
                m_caClass = bundle.loadClass(CONFIGURATION_ADMIN_CLASS);
                m_caGetConfiguration = m_caClass.getMethod("getConfiguration", String.class, String.class);
                m_caListConfigurations = m_caClass.getMethod("listConfigurations", String.class);

                m_configurationClass = bundle.loadClass(CONFIGURATION_CLASS);
                m_configurationGetProperties = m_configurationClass.getMethod("getProperties");
                Method configurationGetProcessedProperties = null;
                try {
                    // try for the 1.6 method
                    configurationGetProcessedProperties = m_configurationClass.getMethod("getProcessedProperties", ServiceReference.class);
                }
                catch (NoSuchMethodException nsme) {
                    // ignore
                }
                m_configurationGetProcessedProperties = configurationGetProcessedProperties;

                m_ceClass = bundle.loadClass(CONFIGURATION_EVENT_CLASS);
                m_ceGetPid = m_ceClass.getMethod("getPid");
                m_ceGetType = m_ceClass.getMethod("getType");

                m_clClass = bundle.loadClass(CONFIGURATION_LISTENER_CLASS);
                m_clConfigurationEvent = m_clClass.getMethod("configurationEvent", m_ceClass);
            }
            catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) {
                throw new IllegalStateException(
                    "Failure reflecting over API from Configuration Admin service bundle", e);
            }

            m_registration = m_context.registerService(CONFIGURATION_LISTENER_CLASS, configurationListenerProxy(), null);
        }

        private Object configurationListenerProxy() {
            ClassLoader classLoader = m_caReference.getBundle().adapt(BundleWiring.class).getClassLoader();

            return Proxy.newProxyInstance(
                classLoader, new Class<?>[] {m_clClass},
                new InvocationHandler() {

                    @SuppressWarnings("unchecked")
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        if (method.equals(m_clConfigurationEvent)) {
                            String pid = (String)m_ceGetPid.invoke(args[0]);
                            if(!pid.startsWith(LOGGER_ADMIN_PID)) {
                            	return null;
                            }
                            String configName = null;
                            String location = "?";
                            if (pid.startsWith(LOGGER_ADMIN_PID_PREFIX)) {
                                configName = pid.substring(LOGGER_ADMIN_PID_PREFIX.length());
                                if (configName.contains("|") && (configName.split("|").length == 3)) {
                                    String[] parts = configName.split("|");
                                    location = parts[2];
                                }
                            }

                            switch ((int)m_ceGetType.invoke(args[0])) {
                                case 2: // CM_DELETED
                                    m_loggerAdmin.updateConfiguration(configName, null);
                                    break;
                                default:
                                    Object configObj = m_caGetConfiguration.invoke(m_ca, pid, location);
                                    Object propertiesObj = getProperties(configObj);
                                    m_loggerAdmin.updateConfiguration(configName, (Dictionary<String, Object>)propertiesObj);
                            }
                        }

                        return null;
                    }
                }
            );
        }

        @SuppressWarnings("unchecked")
        private Dictionary<String, Object> getProperties(Object configObj) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            if (m_configurationGetProcessedProperties != null) {
                return (Dictionary<String, Object>) m_configurationGetProcessedProperties.invoke(configObj, m_caReference);
            }
            return (Dictionary<String, Object>) m_configurationGetProperties.invoke(configObj);
        }

        @SuppressWarnings("rawtypes")
        private List<Dictionary> listConfigurations(String filter) {
            try {
                Object result = m_caListConfigurations.invoke(m_ca, filter);
                if (result != null) {
                    List<Dictionary> dictionaries = new ArrayList<>();
                    for (Object configObj : (Object[])result) {
                        dictionaries.add(getProperties(configObj));
                    }
                    return dictionaries;
                }
            }
            catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                if (e instanceof InvocationTargetException) {
                    m_log.log(getClass().getName(), m_context.getBundle(), null, LogLevel.ERROR, "An error occurred reflecting on ConfigurationAdmin.", ((InvocationTargetException)e).getTargetException());
                }
                else {
                    m_log.log(getClass().getName(), m_context.getBundle(), null, LogLevel.ERROR, "An error occurred reflecting on ConfigurationAdmin.", e);
                }
            }
            return Collections.emptyList();
        }

        private void unregister() {
            m_registration.unregister();
        }

        @SuppressWarnings({ "rawtypes", "unchecked" })
        private void updateContext(String name, String pid, boolean delete) {
            List<Dictionary> configurations = listConfigurations(String.format("(%s=%s)", Constants.SERVICE_PID, pid));

            if (!configurations.isEmpty()) {
                m_loggerAdmin.updateConfiguration(name, delete ? null : configurations.get(0));
            }
        }

    }

    class CLCustomizer implements ServiceTrackerCustomizer<Object, CLProxy> {

        @Override
        public CLProxy addingService(ServiceReference<Object> reference) {
            CLProxy clProxy = new CLProxy(reference);

            // configure ROOT context
            clProxy.updateContext(null, "org.osgi.service.log.admin", false);

            // configure bundle contexts
            for (String name : m_loggerAdmin.getLoggerContextNames()) {
                String pid = "org.osgi.service.log.admin|" + name;
                clProxy.updateContext(name, pid, false);
            }

            return clProxy;
        }

        @Override
        public void modifiedService(ServiceReference<Object> reference, CLProxy clProxy) {
        }

        @Override
        public void removedService(ServiceReference<Object> reference, CLProxy clProxy) {
            // un-configure bundle contexts
            for (String name : m_loggerAdmin.getLoggerContextNames()) {
                String pid = "org.osgi.service.log.admin|" + name;
                clProxy.updateContext(name, pid, true);
            }

            // un-configure ROOT context
            clProxy.updateContext(null, "org.osgi.service.log.admin", true);

            // un-register the configuration listener
            clProxy.unregister();
        }

    }

}
