/*
 * 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.dm.runtime;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.felix.dm.Component;
import org.apache.felix.dm.DependencyManager;
import org.osgi.framework.Bundle;
import org.osgi.service.packageadmin.PackageAdmin;

/**
 * This class parses service descriptors generated by the annotation bnd processor.
 * The descriptors are located under META-INF/dependencymanager directory. Such files are actually 
 * referenced by a specific "DependendencyManager-Component" manifest header.
 * 
 * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
 */
public class DependencyManagerRuntime
{
    /**
     * Map between bundles and their corresponding DependencyManager objects used to create bundle's components.
     * Notice that we can safely use this map without synchronization because we are relying on the new DM4 thread
     * model which serialize all component events safely.
     */
    private final Map<Bundle, DependencyManager> m_managers = new HashMap<Bundle, DependencyManager>();
    
    /**
     * Parser used to scan component descriptors defined in bundles meta data.
     */
    private final DescriptorParser m_parser;
    
    /**
     * We use the PackageAdmin service to allow support for annotations in fragment bundles.
     */
    private volatile PackageAdmin m_packageAdmin;
    
    /**
     * Our constructor. We'll initialize here our DM component builders.
     */
    public DependencyManagerRuntime()
    {
        // Instantiates our descriptor parser, and register our service builders into it.
        m_parser = new DescriptorParser();
        m_parser.addBuilder(new ComponentBuilder());
        m_parser.addBuilder(new AspectServiceBuilder());
        m_parser.addBuilder(new AdapterServiceBuilder());
        m_parser.addBuilder(new BundleAdapterServiceBuilder());
        m_parser.addBuilder(new FactoryConfigurationAdapterServiceBuilder());
        m_parser.addBuilder(new ResourceAdapterServiceBuilder());
    }
    
    /**
     * Return our Object Composition (the Activator will inject dependencies into it)
     */
    protected Object[] getComposition()
    {
        return new Object[] { this, Log.instance() };
    }

    /**
     * Starts our Service (at this point, we have been injected with our bundle context, as well
     * as with our log service. We'll listen to bundle start/stop events (we implement the 
     * SynchronousBundleListener interface).
     */
    protected void start()
    {
        Log.instance().info("Starting Dependency Manager annotation runtime.");
    }

    /**
     * Stops our service. We'll stop all activated DependencyManager services.
     */
    protected void stop()
    {
        Log.instance().info("Runtime: stopping services");
        for (DependencyManager dm : m_managers.values())
        {
            List<Component> services = new ArrayList<Component>(dm.getComponents());
            for (Component service : services)
            {
                dm.remove(service);
            }
        }

        m_managers.clear();
    }

    /**
     * Load the DM descriptors from the started bundle. We also check possible fragments 
     * attached to the bundle, which might also contain some DM descriptors. 
     * @param bundle the started bundle which contains a DependencyManager-Component header
     */
    protected void bundleStarted(Bundle bundle)
    {
        Log.instance().info("Scanning started bundle %s", bundle.getSymbolicName());
        List<URL> descriptorURLs = new ArrayList<URL>();
        collectDescriptors(bundle, descriptorURLs);
        Bundle[] fragments = m_packageAdmin.getFragments(bundle);
        if (fragments != null)
        {
            for (Bundle fragment : fragments)
            {
                collectDescriptors(fragment, descriptorURLs);
            }
        }
        for (URL descriptorURL : descriptorURLs)
        {
            loadDescriptor(bundle, descriptorURL);
        }
    }
    
    /**
     * Unregisters all services for a stopping bundle.
     * @param b
     */
    protected void bundleStopped(Bundle b)
    {
        Log.instance().info("Runtime: Removing services from stopping bundle: %s", b.getSymbolicName());
        DependencyManager dm = m_managers.remove(b);
        if (dm != null)
        {
            List<Component> services = new ArrayList<Component>(dm.getComponents());
            for (Component service : services)
            {
                Log.instance().info("Runtime: Removing service: %s", service);
                dm.remove(service);
            }
        }
    }

    /**
     * Collect all descriptors found from a given bundle, including its possible attached fragments.
     * @param bundle a started bundle containing some DM descriptors
     * @param out the list of descriptors' URLS found from the started bundle, as well as from possibly
     *        attached fragments.
     */
    private void collectDescriptors(Bundle bundle, List<URL> out) {
        String descriptorPaths = (String) bundle.getHeaders().get("DependencyManager-Component");
        if (descriptorPaths == null)
        {
            return;
        }

        for (String descriptorPath : descriptorPaths.split(","))
        {
            URL descriptorURL = bundle.getEntry(descriptorPath);
            if (descriptorURL == null)
            {
                Log.instance()
                        .error("Runtime: " + "DependencyManager component descriptor not found: %s",
                               descriptorPath);
                continue;
            }
            out.add(descriptorURL);
        }        
    }
    
    /**
     * Load a DependencyManager component descriptor from a given bundle.
     * @param b
     * @param descriptorURL
     */
    private void loadDescriptor(Bundle b, URL descriptorURL)
    {
        Log.instance().debug("Parsing descriptor %s from bundle %s", descriptorURL, b.getSymbolicName());

        BufferedReader in = null;
        try
        {
            in = new BufferedReader(new InputStreamReader(descriptorURL.openStream()));
            DependencyManager dm = m_managers.get(b);
            if (dm == null)
            {
                dm = new DependencyManager(b.getBundleContext());
                m_managers.put(b, dm);
            }

            m_parser.parse(in, b, dm);
        }

        catch (Throwable t)
        {
            Log.instance().error("Runtime: Error while parsing descriptor %s from bundle %s",
                                 t,
                                 descriptorURL,
                                 b.getSymbolicName());
        }

        finally
        {
            if (in != null)
            {
                try
                {
                    in.close();
                }
                catch (IOException ignored)
                {
                }
            }
        }
    }
}