/*
 * 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 WARRANTIESOR 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.aries.jmx.core.whiteboard;

import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.aries.jmx.Logger;
import org.apache.aries.jmx.framework.BundleState;
import org.apache.aries.jmx.framework.Framework;
import org.apache.aries.jmx.framework.PackageState;
import org.apache.aries.jmx.framework.ServiceState;
import org.apache.aries.jmx.util.ObjectNameUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceFactory;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.jmx.framework.BundleStateMBean;
import org.osgi.jmx.framework.FrameworkMBean;
import org.osgi.jmx.framework.PackageStateMBean;
import org.osgi.jmx.framework.ServiceStateMBean;
import org.osgi.jmx.service.cm.ConfigurationAdminMBean;
import org.osgi.jmx.service.permissionadmin.PermissionAdminMBean;
import org.osgi.jmx.service.provisioning.ProvisioningServiceMBean;
import org.osgi.jmx.service.useradmin.UserAdminMBean;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.packageadmin.PackageAdmin;
import org.osgi.service.permissionadmin.PermissionAdmin;
import org.osgi.service.provisioning.ProvisioningService;
import org.osgi.service.startlevel.StartLevel;
import org.osgi.service.useradmin.UserAdmin;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;

public class Activator implements BundleActivator, ServiceTrackerCustomizer
{
  private ServiceTracker tracker;
  private BundleContext ctx;
  private ConcurrentMap<Long, ServiceRegistration> _provisioningMBeans = new ConcurrentHashMap<Long, ServiceRegistration>();
  private ConcurrentMap<Long, ServiceRegistration> _userAdminMBeans = new ConcurrentHashMap<Long, ServiceRegistration>();
  private ConcurrentMap<Long, ServiceRegistration> _configAdminMBeans = new ConcurrentHashMap<Long, ServiceRegistration>();

  private AtomicReference<ServiceRegistration> _serviceStateMbean = new AtomicReference<ServiceRegistration>();
  private AtomicReference<ServiceRegistration> _permissionAdminMbean = new AtomicReference<ServiceRegistration>();
  private AtomicReference<ServiceRegistration> _packageStateMbean = new AtomicReference<ServiceRegistration>();
  private AtomicReference<ServiceRegistration> _bundleState = new AtomicReference<ServiceRegistration>();
  private AtomicReference<ServiceRegistration> _framework = new AtomicReference<ServiceRegistration>();

  private AtomicReference<ServiceReference> _startLevel = new AtomicReference<ServiceReference>();
  private AtomicReference<ServiceReference> _packageAdmin = new AtomicReference<ServiceReference>();

  private static final String PACKAGE_ADMIN = "org.osgi.service.packageadmin.PackageAdmin";
  private static final String START_LEVEL = "org.osgi.service.startlevel.StartLevel";
  private static final String PERMISSION_ADMIN = "org.osgi.service.permissionadmin.PermissionAdmin";
  private static final String CONFIG_ADMIN = "org.osgi.service.cm.ConfigurationAdmin";
  private static final String USER_ADMIN = "org.osgi.service.useradmin.UserAdmin";
  private static final String PROVISIONING_SERVICE = "org.osgi.service.provisioning.ProvisioningService";

  private Logger logger;

  private class MBeanServiceProxy<T> implements ServiceFactory
  {
    private Factory<T> objectFactory;
    private AtomicReference<T> result = new AtomicReference<T>();

    private MBeanServiceProxy(Factory<T> factory) {
      objectFactory = factory;
    }

    public Object getService(Bundle bundle, ServiceRegistration registration)
    {
      if (result.get() == null) {
        result.compareAndSet(null, objectFactory.create());
      }
      return result.get();
    }

    public void ungetService(Bundle bundle, ServiceRegistration registration, Object service)
    {
    }
  }

  private interface Factory<T>
  {
    public abstract T create();
  }

  private abstract class BaseFactory<T> implements Factory<T>
  {
    public abstract T create(PackageAdmin pa, StartLevel sl);
    public final T create()
    {
      StartLevel sl = null;
      PackageAdmin pa = null;

      ServiceReference slRef = _startLevel.get();
      if (slRef != null) {
        sl = (StartLevel) ctx.getService(slRef);
      }
      ServiceReference paRef = _packageAdmin.get();

      if (paRef != null) {
        pa = (PackageAdmin) ctx.getService(paRef);
      }

      if (pa == null) {
        ctx.ungetService(slRef);
      }

      if (sl != null && pa != null) {
        return create(pa, sl);
      } else {
        return null;
      }
    }
  }

  public void start(BundleContext context) throws Exception
  {
    ctx = context;
    logger = new Logger(ctx);

    Filter filter = getFilter(context, PACKAGE_ADMIN, START_LEVEL,
        PERMISSION_ADMIN, CONFIG_ADMIN, USER_ADMIN,
        PROVISIONING_SERVICE);

    tracker = new ServiceTracker(context, filter, this);
    tracker.open();

    registerMBean(ServiceStateMBean.class.getName(), new Factory<ServiceStateMBean>() {
      public ServiceStateMBean create()
      {
        return new ServiceState(ctx, logger);
      }
    }, ServiceStateMBean.OBJECTNAME, _serviceStateMbean );
  }

  private Filter getFilter(BundleContext ctx, String ... services) throws InvalidSyntaxException
  {
    StringBuilder builder = new StringBuilder("(|");

    for (String type : services) {
      builder.append('(');
      builder.append(Constants.OBJECTCLASS);
      builder.append('=');
      builder.append(type);
      builder.append(')');
    }

    builder.append(')');
    return ctx.createFilter(builder.toString());
  }

  public void stop(BundleContext context) throws Exception
  {
    tracker.close();
  }

  public Object addingService(ServiceReference reference)
  {
    Object tracked = null;

    String[] types = (String[]) reference.getProperty(Constants.OBJECTCLASS);

    for (String t : types) {
      if (PACKAGE_ADMIN.equals(t)) {
        foundPackageAdmin(reference);
        tracked = reference;
      } else if (START_LEVEL.equals(t)) {
        foundStartLevel(reference);
        tracked = reference;
      } else if (PERMISSION_ADMIN.equals(t)) {
        foundPermissionAdmin(reference);
        tracked = reference;
      } else if (CONFIG_ADMIN.equals(t)) {
        foundConfigAdmin(reference);
        tracked = reference;
      } else if (USER_ADMIN.equals(t)) {
        foundUserAdmin(reference);
        tracked = reference;
      } else if (PROVISIONING_SERVICE.equals(t)) {
        foundProvisioningService(reference);
        tracked = reference;
      }
    }

    return tracked;
  }

  private <T> void registerMBean(String type, Factory<T> factory, String objectName, AtomicReference<ServiceRegistration> result)
  {
    synchronized (result) {
      ServiceRegistration reg = registerAnMbean(type, factory, objectName);

      if (!!!result.compareAndSet(null, reg)) {
        reg.unregister();
      }
    }
  }

  private <T> void registerMBean(String type, Factory<T> factory, String objectName, ConcurrentMap<Long, ServiceRegistration> mbeans,
      ServiceReference referencedServices, String underlyingType)
  {
    try {
      Class.forName(underlyingType);
      if (referencedServices.isAssignableTo(ctx.getBundle(), underlyingType)) {
        ServiceRegistration reg = registerAnMbean(type, factory, objectName);

        Long id = (Long) reg.getReference().getProperty(Constants.SERVICE_ID);
        mbeans.put(id, reg);
      }
    } catch (ClassNotFoundException e) {
    }
  }

  private <T> ServiceRegistration registerAnMbean(String type, Factory<T> factory, String objectName)
  {
    Hashtable<String, Object> properties = new Hashtable<String, Object>();
    properties.put("jmx.objectname", ObjectNameUtils.createFullObjectName(ctx, objectName));

    Object service = new MBeanServiceProxy<T>(factory);

    ServiceRegistration reg = ctx.registerService(type, service, properties);
    return reg;
  }

  private void foundPermissionAdmin(final ServiceReference reference)
  {
    registerMBean(PermissionAdminMBean.class.getName(), new Factory<PermissionAdminMBean>() {
      public PermissionAdminMBean create()
      {
        PermissionAdmin service = (PermissionAdmin) ctx.getService(reference);

        if (service == null) return null;
        else return new org.apache.aries.jmx.permissionadmin.PermissionAdmin(service);
      }
    }, PermissionAdminMBean.OBJECTNAME, _permissionAdminMbean);
  }

  private void foundProvisioningService(final ServiceReference reference)
  {
    registerMBean(ProvisioningServiceMBean.class.getName(), new Factory<ProvisioningServiceMBean>() {
        public ProvisioningServiceMBean create()
        {
          ProvisioningService service = (ProvisioningService) ctx.getService(reference);

          if (service == null) return null;
          else return new org.apache.aries.jmx.provisioning.ProvisioningService(service);
        }
      }, ProvisioningServiceMBean.OBJECTNAME, _provisioningMBeans, reference, PROVISIONING_SERVICE);
  }

  private void foundUserAdmin(final ServiceReference reference)
  {
    try {
      Class.forName(USER_ADMIN);
      if (reference.isAssignableTo(ctx.getBundle(), USER_ADMIN)) {
        registerMBean(UserAdminMBean.class.getName(), new Factory<UserAdminMBean>() {
          public UserAdminMBean create()
          {
            UserAdmin service = (UserAdmin) ctx.getService(reference);

            if (service == null) return null;
            else return new org.apache.aries.jmx.useradmin.UserAdmin(service);
          }
        }, UserAdminMBean.OBJECTNAME, _userAdminMBeans, reference, USER_ADMIN);
      }
    } catch (ClassNotFoundException e) {
    }
  }

  private void foundConfigAdmin(final ServiceReference reference)
  {
    registerMBean(ConfigurationAdminMBean.class.getName(), new Factory<ConfigurationAdminMBean>() {
      public ConfigurationAdminMBean create()
      {
        ConfigurationAdmin service = (ConfigurationAdmin) ctx.getService(reference);

        if (service == null) return null;
        else return new org.apache.aries.jmx.cm.ConfigurationAdmin(service);
      }
    }, ConfigurationAdminMBean.OBJECTNAME, _configAdminMBeans, reference, CONFIG_ADMIN);
  }

  private void foundStartLevel(final ServiceReference reference)
  {
    if (_startLevel.compareAndSet(null, reference)) {
      registerBundleStateAndFrameworkIfPossible();
    }
  }

  private void foundPackageAdmin(final ServiceReference reference)
  {
    registerMBean(PackageStateMBean.class.getName(), new Factory<PackageStateMBean>() {
      public PackageStateMBean create()
      {
        PackageAdmin service = (PackageAdmin) ctx.getService(reference);

        if (service == null) return null;
        else return new PackageState(ctx, service);
      }
    }, PackageStateMBean.OBJECTNAME, _packageStateMbean);

    if (_packageAdmin.compareAndSet(null, reference)) {
      registerBundleStateAndFrameworkIfPossible();
    }
  }

  // This method is synchronized to ensure that notification of StartLevel and PackageAdmin
  // on different threads at the same time doesn't cause problems. It only affects these services
  // so it shouldn't be too expensive.
  private synchronized void registerBundleStateAndFrameworkIfPossible()
  {
      if (_bundleState.get() == null && _startLevel.get() != null && _packageAdmin.get() != null) {
        registerMBean(BundleStateMBean.class.getName(), new BaseFactory<BundleStateMBean>() {
          @Override
          public BundleStateMBean create(PackageAdmin pa, StartLevel sl)
          {
            return new BundleState(ctx, pa, sl, logger);
          }
        }, BundleStateMBean.OBJECTNAME, _bundleState);
      }
      if (_framework.get() == null && _startLevel.get() != null && _packageAdmin.get() != null) {
        registerMBean(FrameworkMBean.class.getName(), new BaseFactory<FrameworkMBean>() {
          @Override
          public FrameworkMBean create(PackageAdmin pa, StartLevel sl)
          {
            return new Framework(ctx, sl, pa);
          }
        }, FrameworkMBean.OBJECTNAME, _framework);
      }
  }

  public void modifiedService(ServiceReference reference, Object service)
  {
  }

  public void removedService(ServiceReference reference, Object service)
  {
    String[] types = (String[]) reference.getProperty(Constants.OBJECTCLASS);

    for (String t : types) {
      if (PACKAGE_ADMIN.equals(t)) {
        lostPackageAdmin(reference);
      } else if (START_LEVEL.equals(t)) {
        lostStartLevel(reference);
      } else if (PERMISSION_ADMIN.equals(t)) {
        lostPermissionAdmin(reference);
      } else if (CONFIG_ADMIN.equals(t)) {
        lostConfigAdmin(reference);
      } else if (USER_ADMIN.equals(t)) {
        lostUserAdmin(reference);
      } else if (PROVISIONING_SERVICE.equals(t)) {
        lostProvisioningService(reference);
      }
    }

  }

  private void lostProvisioningService(ServiceReference reference)
  {
    unregister(reference, _provisioningMBeans);
  }

  private void lostUserAdmin(ServiceReference reference)
  {
    unregister(reference, _userAdminMBeans);
  }

  private void lostConfigAdmin(ServiceReference reference)
  {
    unregister(reference, _configAdminMBeans);
  }

  private void unregister(ServiceReference reference, ConcurrentMap<Long, ServiceRegistration> mbeans)
  {
    Long id = (Long) reference.getProperty(Constants.SERVICE_ID);
    ServiceRegistration reg = mbeans.remove(id);
    if (reg != null) reg.unregister();
  }

  private void lostPermissionAdmin(ServiceReference reference)
  {
    safeUnregister(_permissionAdminMbean);
  }

  private void lostStartLevel(ServiceReference reference)
  {
    if (_startLevel.compareAndSet(reference, null)) {
      safeUnregister(_bundleState);
      safeUnregister(_framework);
    }
  }

  private void lostPackageAdmin(ServiceReference reference)
  {
    if (_packageAdmin.compareAndSet(reference, null)) {
      safeUnregister(_bundleState);
      safeUnregister(_framework);

      safeUnregister(_packageStateMbean);
    }
  }

  private void safeUnregister(AtomicReference<ServiceRegistration> atomicRegistration)
  {
    ServiceRegistration reg = atomicRegistration.getAndSet(null);
    if (reg != null) reg.unregister();
  }
}