/*
 * 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.activemq.artemis.osgi;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.activemq.artemis.api.core.Interceptor;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.api.core.management.ObjectNameBuilder;
import org.apache.activemq.artemis.core.config.FileDeploymentManager;
import org.apache.activemq.artemis.core.config.StoreConfiguration;
import org.apache.activemq.artemis.core.config.StoreConfiguration.StoreType;
import org.apache.activemq.artemis.core.config.impl.FileConfiguration;
import org.apache.activemq.artemis.core.config.storage.DatabaseStorageConfiguration;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.core.server.ActiveMQComponent;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.management.ArtemisMBeanServerBuilder;
import org.apache.activemq.artemis.core.server.management.ArtemisMBeanServerGuard;
import org.apache.activemq.artemis.core.server.management.HawtioSecurityControl;
import org.apache.activemq.artemis.core.server.management.JMXAccessControlList;
import org.apache.activemq.artemis.core.server.management.impl.HawtioSecurityControlImpl;
import org.apache.activemq.artemis.jms.server.config.impl.FileJMSConfiguration;
import org.apache.activemq.artemis.spi.core.protocol.ProtocolManagerFactory;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.util.tracker.ServiceTracker;

import javax.management.ObjectName;

@SuppressWarnings({"unchecked", "rawtypes"})
@Component(configurationPid = "org.apache.activemq.artemis", configurationPolicy = ConfigurationPolicy.REQUIRE)
public class OsgiBroker {

   private String name;
   private String configurationUrl;
   private String rolePrincipalClass;
   private Map<String, ActiveMQComponent> components;
   private Map<String, ServiceRegistration<?>> registrations;
   private ServiceTracker tracker;
   private ServiceTracker dataSourceTracker;

   @Activate
   public void activate(ComponentContext cctx) throws Exception {
      final BundleContext context = cctx.getBundleContext();
      final Dictionary<String, Object> properties = cctx.getProperties();
      configurationUrl = getMandatory(properties, "config");
      name = getMandatory(properties, "name");
      rolePrincipalClass = (String) properties.get("rolePrincipalClass");
      String domain = getMandatory(properties, "domain");
      ActiveMQJAASSecurityManager security = new ActiveMQJAASSecurityManager(domain);
      if (rolePrincipalClass != null) {
         security.setRolePrincipalClass(rolePrincipalClass);
      }
      String brokerInstance = null;

      String artemisDataDir = System.getProperty("artemis.data");
      if (artemisDataDir != null) {
         brokerInstance = artemisDataDir + "/artemis/" + name;
      } else {
         String karafDataDir = System.getProperty("karaf.data");
         if (karafDataDir != null) {
            brokerInstance = karafDataDir + "/artemis/" + name;
         }
      }


      // todo if we start to pullout more configs from the main config then we
      // should pull out the configuration objects from factories if available
      FileConfiguration configuration = new FileConfiguration();
      if (brokerInstance != null) {
         configuration.setBrokerInstance(new File(brokerInstance));
      }
      FileJMSConfiguration jmsConfiguration = new FileJMSConfiguration();

      FileDeploymentManager fileDeploymentManager = new FileDeploymentManager(configurationUrl);
      fileDeploymentManager.addDeployable(configuration).addDeployable(jmsConfiguration).readConfiguration();

      components = fileDeploymentManager.buildService(security, ManagementFactory.getPlatformMBeanServer(), null);

      final ActiveMQServer server = (ActiveMQServer) components.get("core");

      String[] requiredProtocols = getRequiredProtocols(server.getConfiguration().getAcceptorConfigurations());
      ServerTrackerCallBack callback = new ServerTrackerCallBackImpl(server, context, properties);

      StoreConfiguration storeConfiguration = server.getConfiguration().getStoreConfiguration();
      String dataSourceName = String.class.cast(properties.get("dataSourceName"));

      if (storeConfiguration != null &&
              storeConfiguration.getStoreType() == StoreType.DATABASE && dataSourceName != null &&
              !dataSourceName.isEmpty()) {
         callback.setDataSourceDependency(true);
         String filter = "(&(objectClass=javax.sql.DataSource)(osgi.jndi.service.name=" + dataSourceName + "))";
         DataSourceTracker trackerCust =
                 new DataSourceTracker(name, context, DatabaseStorageConfiguration.class.cast(storeConfiguration),
                         (ServerTrackerCallBack) callback);
         dataSourceTracker = new ServiceTracker(context, context.createFilter(filter), trackerCust);
         dataSourceTracker.open();
      }

      registerMBean();

      ProtocolTracker trackerCust = new ProtocolTracker(name, context, requiredProtocols, callback);
      tracker = new ServiceTracker(context, ProtocolManagerFactory.class, trackerCust);
      tracker.open();
   }

   private void registerMBean() throws Exception {
      ArtemisMBeanServerGuard artemisMBeanServerGuard = createArtemisMBeanServerGuard();

      HawtioSecurityControl control = new HawtioSecurityControlImpl(artemisMBeanServerGuard, null);
      ObjectName objectName = new ObjectName("artemis:area=jmx,name=ArtemisJMXSecurity,type=security");
      if (!ManagementFactory.getPlatformMBeanServer().isRegistered(objectName)) {
         ManagementFactory.getPlatformMBeanServer().registerMBean(control, objectName);
      }
   }

   public ArtemisMBeanServerGuard createArtemisMBeanServerGuard() throws Exception {
      JMXAccessControlList accessControlList = createJMXAccessControlList();
      ArtemisMBeanServerGuard guardHandler = new ArtemisMBeanServerGuard();
      guardHandler.setJMXAccessControlList(accessControlList);
      ArtemisMBeanServerBuilder.setGuard(guardHandler);
      return guardHandler;
   }

   private JMXAccessControlList createJMXAccessControlList() {
      JMXAccessControlList accessControlList = new JMXAccessControlList();
      accessControlList.addToAllowList("org.apache.activemq.artemis", null);
      return accessControlList;
   }

   private String getMandatory(Dictionary<String, ?> properties, String key) {
      String value = (String) properties.get(key);
      if (value == null) {
         throw new IllegalStateException("Property " + key + " must be set");
      }
      return value;
   }

   private String[] getRequiredProtocols(Set<TransportConfiguration> acceptors) {
      ArrayList<String> protocols = new ArrayList<>();
      for (TransportConfiguration acceptor : acceptors) {
         Object protocolsFromAcceptor = acceptor.getParams().get(TransportConstants.PROTOCOLS_PROP_NAME);
         if (protocolsFromAcceptor != null) {
            String[] protocolsSplit = protocolsFromAcceptor.toString().split(",");
            for (String protocol : protocolsSplit) {
               if (!protocols.contains(protocol)) {
                  protocols.add(protocol);
               }
            }
         }
      }
      return protocols.toArray(new String[protocols.size()]);
   }

   @Deactivate
   public void stop() throws Exception {
      tracker.close();
      if (dataSourceTracker != null) {
         dataSourceTracker.close();
      }
   }

   public Map<String, ActiveMQComponent> getComponents() {
      return components;
   }

   /*
    * this makes sure the components are started in the correct order. Its
    * simple at the mo as e only have core and jms but will need impproving if
    * we get more.
    */
   public ArrayList<ActiveMQComponent> getComponentsByStartOrder(Map<String, ActiveMQComponent> components) {
      ArrayList<ActiveMQComponent> activeMQComponents = new ArrayList<>();
      ActiveMQComponent jmsComponent = components.get("jms");
      if (jmsComponent != null) {
         activeMQComponents.add(jmsComponent);
      }
      activeMQComponents.add(components.get("core"));
      return activeMQComponents;
   }

   public void register(BundleContext context, Dictionary<String, ?> properties) {
      registrations = new HashMap<>();
      for (Map.Entry<String, ActiveMQComponent> component : getComponents().entrySet()) {
         String[] classes = getInterfaces(component.getValue());
         Hashtable<String, Object> props = new Hashtable<>();
         for (Enumeration<String> keyEnum = properties.keys(); keyEnum.hasMoreElements(); ) {
            String key = keyEnum.nextElement();
            Object val = properties.get(key);
            props.put(key, val);
         }
         ServiceRegistration<?> registration = context.registerService(classes, component.getValue(), props);
         registrations.put(component.getKey(), registration);
      }
   }

   private String[] getInterfaces(ActiveMQComponent value) {
      Set<String> interfaces = new HashSet<>();
      getInterfaces(value.getClass(), interfaces);
      return interfaces.toArray(new String[interfaces.size()]);
   }

   private void getInterfaces(Class<?> clazz, Set<String> interfaces) {
      for (Class<?> itf : clazz.getInterfaces()) {
         if (interfaces.add(itf.getName())) {
            getInterfaces(itf, interfaces);
         }
      }
      if (clazz.getSuperclass() != null) {
         getInterfaces(clazz.getSuperclass(), interfaces);
      }
   }

   public void unregister() {
      if (registrations != null) {
         for (ServiceRegistration<?> reg : registrations.values()) {
            reg.unregister();
         }
      }
   }

   private class ServerTrackerCallBackImpl implements ServerTrackerCallBack {

      private volatile boolean dataSourceDependency = false;

      private final ActiveMQServer server;
      private final BundleContext context;
      private final Dictionary<String, Object> properties;

      ServerTrackerCallBackImpl(ActiveMQServer server, BundleContext context,
                                Dictionary<String, Object> properties) {
         this.server = server;
         this.context = context;
         this.properties = properties;
      }

      @Override
      public void addFactory(ProtocolManagerFactory<Interceptor> pmf) {
         server.addProtocolManagerFactory(pmf);
      }

      @Override
      public void removeFactory(ProtocolManagerFactory<Interceptor> pmf) {
         server.removeProtocolManagerFactory(pmf);
      }

      @Override
      public void stop() throws Exception {
         ActiveMQComponent[] mqComponents = new ActiveMQComponent[components.size()];
         components.values().toArray(mqComponents);
         for (int i = mqComponents.length - 1; i >= 0; i--) {
            mqComponents[i].stop();
         }
         unregister();
      }

      @Override
      public void start() throws Exception {
         if (!dataSourceDependency) {
            List<ActiveMQComponent> componentsByStartOrder = getComponentsByStartOrder(components);
            for (ActiveMQComponent component : componentsByStartOrder) {
               component.start();
            }
            register(context, properties);
         }
      }

      @Override
      public boolean isStarted() {
         return server.isStarted();
      }



      @Override
      public void setDataSourceDependency(boolean dataSourceDependency) {
         this.dataSourceDependency = dataSourceDependency;
      }


   }

}
