/*
 * 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.clerezza.permissiondescriptions;

import java.net.URL;
import java.security.Permission;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.clerezza.utils.IteratorMerger;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This binds all implementations of
 * <code>PermissionDescriptionsProvider</code>, and gathers their
 * <code>PermissionDescription</code>s. Furthermore the gatherer scans all
 * activaded bundles for
 * <code>Permission</code>s which are annonated with the
 * <code>PermissionInfo</code> annotation and generates
 * <code>PermissionDescription</code>s for them. This service provides methods
 * to retrieve the gathered
 * <code>PermissionDescription</code>s and also methods to retrieve all
 * unannotated
 * <code>Permission</code> found in the activated bundles. If new bundles are
 * started then they are also scanned.
 *
 * @author mir
 */
@Component(immediate = true)
@Service(PermissionGatherer.class)
@Reference(name = "permissionProvider", policy = ReferencePolicy.DYNAMIC,
        referenceInterface = PermissionDescriptionsProvider.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE)
public class PermissionGatherer implements BundleListener {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private Map<Bundle, Collection<Class<? extends Permission>>> bundle2PermissionClassesMap =
            Collections.synchronizedMap(new HashMap<Bundle, Collection<Class<? extends Permission>>>());
    private Map<Bundle, Collection<PermissionDescripton>> bundle2PermissionDescriptorsMap =
            Collections.synchronizedMap(new HashMap<Bundle, Collection<PermissionDescripton>>());
    private ComponentContext componentContext;
    /**
     * stores all
     * <code>ServiceReference</code>s of
     * <code>PermissionDescriptionsProvider</code>s that were bound before the
     * PermissionGatherer was activated.
     */
    private Set<ServiceReference> serviceReferenceStore = new HashSet<ServiceReference>();

    public Map<Bundle, Collection<PermissionDescripton>> getPermissionDescriptorsPerBundles() {
        return Collections.unmodifiableMap(bundle2PermissionDescriptorsMap);
    }

    public Iterator<PermissionDescripton> getAllPermissionDescriptors() {
        return new IteratorMerger<PermissionDescripton>(bundle2PermissionDescriptorsMap.values());
    }

    public Map<Bundle, Collection<Class<? extends Permission>>> getPermissionsPerBundles() {
        return Collections.unmodifiableMap(bundle2PermissionClassesMap);
    }

    public Iterator<Class<? extends Permission>> getAllPermissions() {
        return new IteratorMerger<Class<? extends Permission>>(
                bundle2PermissionClassesMap.values());
    }

    protected void activate(final ComponentContext componentContext) {
        this.componentContext = componentContext;
        registerFromServiceReferenceStore();
        componentContext.getBundleContext().addBundleListener(this);
        logger.debug("Start registering permissions from activated bundles");
        registerPermissionsFromActivatedBundles(componentContext);
        logger.debug("Registered permissions from activated bundles");
    }

    synchronized private void registerFromServiceReferenceStore() {
        if (componentContext != null) {

            for (ServiceReference ref : serviceReferenceStore) {
                this.registerPermissionDescriptorsProvider(ref);
            }
            serviceReferenceStore.clear();
        }
    }

    synchronized protected void bindPermissionProvider(ServiceReference serviceReference) {
        if (componentContext != null) {
            registerPermissionDescriptorsProvider(serviceReference);
        } else {
            serviceReferenceStore.add(serviceReference);
        }

    }

    synchronized protected void unbindPermissionProvider(ServiceReference serviceReference) {

        if (!serviceReferenceStore.remove(serviceReference)) {
            PermissionDescriptionsProvider permissionProvider = (PermissionDescriptionsProvider) componentContext.locateService("permissionProvider", serviceReference);
            Bundle bundle = serviceReference.getBundle();

            Collection<PermissionDescripton> permissionDescriptiors =
                    bundle2PermissionDescriptorsMap.get(bundle);
            if (permissionDescriptiors != null) {
                permissionDescriptiors.removeAll(permissionProvider.getPermissionDescriptors());
                if (permissionDescriptiors.isEmpty()) {
                    bundle2PermissionDescriptorsMap.remove(bundle);
                }
            }
        }

    }

    private void registerPermissionDescriptorsProvider(ServiceReference serviceReference) {
        PermissionDescriptionsProvider permissionProvider = (PermissionDescriptionsProvider) componentContext.locateService("permissionProvider", serviceReference);
        if (permissionProvider == null) {
            return;
        }
        Bundle bundle = serviceReference.getBundle();
        Collection<PermissionDescripton> permissionDescriptiors =
                bundle2PermissionDescriptorsMap.get(bundle);
        if (permissionDescriptiors == null) {
            permissionDescriptiors = new HashSet<PermissionDescripton>();
        }
        permissionDescriptiors.addAll(permissionProvider.getPermissionDescriptors());
        bundle2PermissionDescriptorsMap.put(bundle, permissionDescriptiors);

    }

    protected void deactivate(final ComponentContext componentContext) {
        componentContext.getBundleContext().removeBundleListener(this);
        bundle2PermissionClassesMap.clear();
        bundle2PermissionDescriptorsMap.clear();
    }

    @Override
    public void bundleChanged(BundleEvent event) {
        Bundle bundle = event.getBundle();
        switch (event.getType()) {
            case BundleEvent.STARTED:
                registerPermissions(bundle);
                break;
            case BundleEvent.STOPPED:
                unregisterPermissions(bundle);
                break;
        }
    }

    private void registerPermissionsFromActivatedBundles(ComponentContext componentContext) {
        Bundle[] bundles = componentContext.getBundleContext().getBundles();
        for (Bundle bundle : bundles) {
            if (bundle.getState() == Bundle.ACTIVE) {
                registerPermissions(bundle);
            }
        }
    }

    private void registerPermissions(Bundle bundle) {
        Enumeration<URL> classUrls = bundle.findEntries("/", "*.class", true);
        if (classUrls == null) {
            return;
        }
        Set<Class<? extends Permission>> permissionClassesSet = new HashSet<Class<? extends Permission>>();
        Set<PermissionDescripton> newPermissionDescriptors =
                new HashSet<PermissionDescripton>();
        while (classUrls.hasMoreElements()) {
            URL url = classUrls.nextElement();

            String className = url.getPath();
            int indexOfLastDot = className.lastIndexOf(".");
            className = className.replaceAll("/", ".").substring(1, indexOfLastDot);
            Class<?> clazz;
            try {
                clazz = bundle.loadClass(className);
            } catch (Throwable t) {
                //we just ignore classes we coudln't read
                continue;
            }
            if (Permission.class.isAssignableFrom(clazz)) {
                PermissionInfo permissionInfo = clazz.getAnnotation(PermissionInfo.class);
                if (permissionInfo != null) {
                    newPermissionDescriptors.add(new PermissionDescripton(permissionInfo.value(),
                            permissionInfo.description(), clazz.getResource(permissionInfo.icon()),
                            (Class<? extends Permission>) clazz,
                            getJavaPermissionString(clazz)));
                } else {
                    permissionClassesSet.add((Class<? extends Permission>) clazz);
                }
            }
        }
        if (!permissionClassesSet.isEmpty()) {
            bundle2PermissionClassesMap.put(bundle, permissionClassesSet);
        }
        if (!newPermissionDescriptors.isEmpty()) {

            Collection<PermissionDescripton> permissionDescriptiors =
                    bundle2PermissionDescriptorsMap.get(bundle);
            if (permissionDescriptiors == null) {
                permissionDescriptiors = newPermissionDescriptors;
            } else {
                permissionDescriptiors.addAll(newPermissionDescriptors);
            }
            bundle2PermissionDescriptorsMap.put(bundle, permissionDescriptiors);
        }
    }

    private void unregisterPermissions(Bundle bundle) {
        bundle2PermissionClassesMap.remove(bundle);
        bundle2PermissionDescriptorsMap.remove(bundle);
    }

    private String getJavaPermissionString(Class clazz) {
        return "(" + clazz.getName() + " \"\" \"\")";
    }
}
