//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.osgi.boot.utils.internal;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;

import org.eclipse.jetty.osgi.boot.utils.BundleClassLoaderHelper;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.osgi.framework.Bundle;

/**
 * DefaultBundleClassLoaderHelper
 * <p>
 * Default implementation of the BundleClassLoaderHelper. Uses introspection to
 * support equinox-3.5 and felix-2.0.0
 */
public class DefaultBundleClassLoaderHelper implements BundleClassLoaderHelper
{
    private static final Logger LOG = Log.getLogger(BundleClassLoaderHelper.class);

    private enum OSGiContainerType
    {
        EquinoxOld, EquinoxLuna, FelixOld, Felix403, Concierge
    }

    private static OSGiContainerType osgiContainer;
    private static Class<?> Equinox_BundleHost_Class;
    private static Class<?> Equinox_EquinoxBundle_Class;
    private static Class<?> Felix_BundleImpl_Class;
    private static Class<?> Felix_BundleWiring_Class;
    //old equinox
    private static Method Equinox_BundleHost_getBundleLoader_method;
    private static Method Equinox_BundleLoader_createClassLoader_method;
    //new equinox
    private static Method Equinox_EquinoxBundle_getModuleClassLoader_Method;

    //new felix
    private static Method Felix_BundleImpl_Adapt_Method;
    //old felix
    private static Field Felix_BundleImpl_m_Modules_Field;
    private static Field Felix_ModuleImpl_m_ClassLoader_Field;
    private static Method Felix_BundleWiring_getClassLoader_Method;

    // Concierge
    private static Class<?> Concierge_BundleImpl_Class;
    private static Class<?> Concierge_BundleWiring_Class;
    private static Method Concierge_BundleImpl_Adapt_Method;
    private static Method Concierge_BundleWiring_getClassLoader_Method;

    private static void checkContainerType(Bundle bundle)
    {
        if (osgiContainer != null)
            return;

        try
        {
            Equinox_BundleHost_Class = bundle.getClass().getClassLoader().loadClass("org.eclipse.osgi.framework.internal.core.BundleHost");
            osgiContainer = OSGiContainerType.EquinoxOld;
            return;
        }
        catch (ClassNotFoundException e)
        {
            LOG.ignore(e);
        }

        try
        {
            Equinox_EquinoxBundle_Class = bundle.getClass().getClassLoader().loadClass("org.eclipse.osgi.internal.framework.EquinoxBundle");
            osgiContainer = OSGiContainerType.EquinoxLuna;
            return;
        }
        catch (ClassNotFoundException e)
        {
            LOG.ignore(e);
        }

        try
        {
            //old felix or new felix?
            Felix_BundleImpl_Class = bundle.getClass().getClassLoader().loadClass("org.apache.felix.framework.BundleImpl");
            try
            {
                Felix_BundleImpl_Adapt_Method = Felix_BundleImpl_Class.getDeclaredMethod("adapt", Class.class);
                osgiContainer = OSGiContainerType.Felix403;
                return;
            }
            catch (NoSuchMethodException e)
            {
                osgiContainer = OSGiContainerType.FelixOld;
                return;
            }
        }
        catch (ClassNotFoundException e)
        {
            LOG.ignore(e);
        }

        try
        {
            Concierge_BundleImpl_Class = bundle.getClass().getClassLoader().loadClass("org.eclipse.concierge.BundleImpl");
            osgiContainer = OSGiContainerType.Concierge;
            return;
        }
        catch (ClassNotFoundException e)
        {
            LOG.ignore(e);
        }

        LOG.warn("Unknown OSGi container type");
        return;
    }

    /**
     * Assuming the bundle is started.
     *
     * @param bundle the bundle
     * @return classloader object
     */
    @Override
    public ClassLoader getBundleClassLoader(Bundle bundle)
    {
        String bundleActivator = (String)bundle.getHeaders().get("Bundle-Activator");

        if (bundleActivator == null)
        {
            bundleActivator = (String)bundle.getHeaders().get("Jetty-ClassInBundle");
        }
        if (bundleActivator != null)
        {
            try
            {
                return bundle.loadClass(bundleActivator).getClassLoader();
            }
            catch (ClassNotFoundException e)
            {
                LOG.warn(e);
            }
        }

        // resort to introspection     
        return getBundleClassLoaderForContainer(bundle);
    }

    /**
     *
     */
    private ClassLoader getBundleClassLoaderForContainer(Bundle bundle)
    {
        checkContainerType(bundle);
        if (osgiContainer == null)
        {
            LOG.warn("No classloader for unknown OSGi container type");
            return null;
        }

        switch (osgiContainer)
        {
            case EquinoxOld:
            case EquinoxLuna:
            {
                return internalGetEquinoxBundleClassLoader(bundle);
            }

            case FelixOld:
            case Felix403:
            {
                return internalGetFelixBundleClassLoader(bundle);
            }

            case Concierge:
            {
                return internalGetConciergeBundleClassLoader(bundle);
            }

            default:
            {
                LOG.warn("No classloader found for bundle " + bundle.getSymbolicName());
                return null;
            }
        }
    }

    /**
     *
     */
    private static ClassLoader internalGetEquinoxBundleClassLoader(Bundle bundle)
    {
        if (osgiContainer == OSGiContainerType.EquinoxOld)
        {
            try
            {
                if (Equinox_BundleHost_getBundleLoader_method == null)
                {
                    Equinox_BundleHost_getBundleLoader_method =
                        Equinox_BundleHost_Class.getDeclaredMethod("getBundleLoader");
                    Equinox_BundleHost_getBundleLoader_method.setAccessible(true);
                }
                Object bundleLoader = Equinox_BundleHost_getBundleLoader_method.invoke(bundle);
                if (Equinox_BundleLoader_createClassLoader_method == null && bundleLoader != null)
                {
                    Equinox_BundleLoader_createClassLoader_method =
                        bundleLoader.getClass().getClassLoader().loadClass("org.eclipse.osgi.internal.loader.BundleLoader").getDeclaredMethod("createClassLoader");
                    Equinox_BundleLoader_createClassLoader_method.setAccessible(true);
                }
                return (ClassLoader)Equinox_BundleLoader_createClassLoader_method.invoke(bundleLoader, new Object[]{});
            }
            catch (ClassNotFoundException t)
            {
                LOG.warn(t);
                return null;
            }
            catch (Throwable t)
            {
                LOG.warn(t);
                return null;
            }
        }

        if (osgiContainer == OSGiContainerType.EquinoxLuna)
        {
            try
            {
                if (Equinox_EquinoxBundle_getModuleClassLoader_Method == null)
                    Equinox_EquinoxBundle_getModuleClassLoader_Method = Equinox_EquinoxBundle_Class.getDeclaredMethod("getModuleClassLoader", Boolean.TYPE);

                Equinox_EquinoxBundle_getModuleClassLoader_Method.setAccessible(true);
                return (ClassLoader)Equinox_EquinoxBundle_getModuleClassLoader_Method.invoke(bundle, new Object[]{
                    Boolean.FALSE
                });
            }
            catch (Exception e)
            {
                LOG.warn(e);
                return null;
            }
        }

        LOG.warn("No classloader for equinox platform for bundle " + bundle.getSymbolicName());
        return null;
    }

    /**
     *
     */
    @SuppressWarnings("unchecked")
    private static ClassLoader internalGetFelixBundleClassLoader(Bundle bundle)
    {

        if (osgiContainer == OSGiContainerType.Felix403)
        {
            try
            {
                if (Felix_BundleWiring_Class == null)
                    Felix_BundleWiring_Class = bundle.getClass().getClassLoader().loadClass("org.osgi.framework.wiring.BundleWiring");

                Felix_BundleImpl_Adapt_Method.setAccessible(true);

                if (Felix_BundleWiring_getClassLoader_Method == null)
                {
                    Felix_BundleWiring_getClassLoader_Method = Felix_BundleWiring_Class.getDeclaredMethod("getClassLoader");
                    Felix_BundleWiring_getClassLoader_Method.setAccessible(true);
                }

                Object wiring = Felix_BundleImpl_Adapt_Method.invoke(bundle, Felix_BundleWiring_Class);
                return (ClassLoader)Felix_BundleWiring_getClassLoader_Method.invoke(wiring);
            }
            catch (Exception e)
            {
                LOG.warn(e);
                return null;
            }
        }

        if (osgiContainer == OSGiContainerType.FelixOld)
        {
            try
            {
                if (Felix_BundleImpl_m_Modules_Field == null)
                {
                    Felix_BundleImpl_m_Modules_Field = Felix_BundleImpl_Class.getDeclaredField("m_modules");
                    Felix_BundleImpl_m_Modules_Field.setAccessible(true);
                }

                // Figure out which version of the modules is exported
                Object currentModuleImpl;

                try
                {
                    Object[] moduleArray = (Object[])Felix_BundleImpl_m_Modules_Field.get(bundle);
                    currentModuleImpl = moduleArray[moduleArray.length - 1];
                }
                catch (Throwable ex)
                {
                    try
                    {
                        List<Object> moduleArray = (List<Object>)Felix_BundleImpl_m_Modules_Field.get(bundle);
                        currentModuleImpl = moduleArray.get(moduleArray.size() - 1);
                    }
                    catch (Exception e)
                    {
                        LOG.warn(e);
                        return null;
                    }
                }

                if (Felix_ModuleImpl_m_ClassLoader_Field == null && currentModuleImpl != null)
                {
                    try
                    {
                        Felix_ModuleImpl_m_ClassLoader_Field = bundle.getClass().getClassLoader().loadClass("org.apache.felix.framework.ModuleImpl").getDeclaredField("m_classLoader");
                        Felix_ModuleImpl_m_ClassLoader_Field.setAccessible(true);
                    }
                    catch (Exception e)
                    {
                        LOG.warn(e);
                        return null;
                    }
                }

                // first make sure that the classloader is ready:
                // the m_classLoader field must be initialized by the
                // ModuleImpl.getClassLoader() private method.
                ClassLoader cl = null;
                try
                {
                    cl = (ClassLoader)Felix_ModuleImpl_m_ClassLoader_Field.get(currentModuleImpl);
                    if (cl != null)
                        return cl;
                }
                catch (Exception e)
                {
                    LOG.warn(e);
                    return null;
                }

                // looks like it was not ready:
                // the m_classLoader field must be initialized by the
                // ModuleImpl.getClassLoader() private method.
                // this call will do that.
                try
                {
                    bundle.loadClass("java.lang.Object");
                    cl = (ClassLoader)Felix_ModuleImpl_m_ClassLoader_Field.get(currentModuleImpl);
                    return cl;
                }
                catch (Exception e)
                {
                    LOG.warn(e);
                    return null;
                }
            }
            catch (Exception e)
            {
                LOG.warn(e);
                return null;
            }
        }

        LOG.warn("No classloader for felix platform for bundle " + bundle.getSymbolicName());
        return null;
    }

    /**
     *
     */
    private static ClassLoader internalGetConciergeBundleClassLoader(Bundle bundle)
    {
        if (osgiContainer == OSGiContainerType.Concierge)
        {
            try
            {
                /**
                 * In Concierge:
                 *
                 * Option A:
                 * <pre>
                 * Concierge concierge = new Concierge(...);
                 * BundleWiring bundleWiring = concierge.getWiring(); // method is public
                 * </pre>
                 * Problem: getWiring not yet implementd
                 *
                 * Option B:
                 * <pre>
                 * Concierge concierge = new Concierge(...);
                 * BundleWiring bundleWiring = concierge.adapt(org.osgi.framework.wiring.BundleWiring);
                 * </pre>
                 * Same approach as done in Felix.
                 *
                 */
                if (Concierge_BundleWiring_Class == null)
                {
                    Concierge_BundleWiring_Class = bundle.getClass().getClassLoader().loadClass("org.osgi.framework.wiring.BundleWiring");
                    Concierge_BundleImpl_Adapt_Method = Concierge_BundleImpl_Class.getMethod("adapt", Class.class);
                    Concierge_BundleImpl_Adapt_Method.setAccessible(true);
                    Concierge_BundleWiring_getClassLoader_Method = Concierge_BundleWiring_Class.getMethod("getClassLoader");
                    Concierge_BundleWiring_getClassLoader_Method.setAccessible(true);
                }

                Object wiring = Concierge_BundleImpl_Adapt_Method.invoke(bundle, Concierge_BundleWiring_Class);
                ClassLoader cl = (ClassLoader)Concierge_BundleWiring_getClassLoader_Method.invoke(wiring);
                return cl;
            }
            catch (Exception e)
            {
                LOG.warn(e);
                return null;
            }
        }

        LOG.warn("No classloader for Concierge platform for bundle " + bundle.getSymbolicName());
        return null;
    }
}
