/**
 * Copyright (C) 2010-14 diirt developers. See COPYRIGHT.TXT
 * All rights reserved. Use is subject to license terms. See LICENSE.TXT
 */
package org.diirt.util.config;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A wrapper for ServiceLoader that works in OSGi as well.
 * <p>
 * OSGi is known to break the ServiceLoader mechanism. Unfortunately, we have
 * not been able to make the standard based solution based on Ares SPI-Fly work.
 * Therefore we have created this wrapper that detects whether the class has
 * been loaded with OSGi. If not, uses the standard ServiceLoader. If so, uses
 * introspection to access the OSGi framework and replicate the functionality.
 * <p>
 * Note that the implementation on OSGi will not give all the nice debugging
 * information that the standard ServiceLoader implementation does. It also is
 * not optimized: makes heavy use of introspection and does not use lazy
 * initialization. It's essentially a hack to make things to work.
 *
 * @author carcassi
 */
public class ServiceLoaderOSGiWrapper {

    public static <T> void load(Class<T> serviceClazz, Logger log, Consumer<T> consumer) {
        log.log(Level.CONFIG, "Fetching {0}s", serviceClazz.getSimpleName());
        int count = 0;
        for (T service : ServiceLoaderOSGiWrapper.load(serviceClazz)) {
            log.log(Level.CONFIG, "Found {0} ({1})", new Object[] {serviceClazz.getSimpleName(), service.getClass().getSimpleName()});
            try {
                consumer.accept(service);
                count++;
            } catch (RuntimeException ex) {
                log.log(Level.WARNING, "Couldn't register " + serviceClazz.getSimpleName() + " (" + service.getClass().getSimpleName() + ")", ex);
            }
        }
        log.log(Level.CONFIG, "Found {0} {1}s", new Object[] {count, serviceClazz.getSimpleName()});
    }

    public static <T> Iterable<T> load(Class<T> serviceClazz) {
        if (isOSGi(serviceClazz)) {
            return loadOSGi(serviceClazz);
        } else {
            return ServiceLoader.load(serviceClazz);
        }
    }

    private static boolean isOSGi(Class<?> serviceClazz) {
        try {
            if (osgi.frameworkUtilClass != null && osgi.getBundle(serviceClazz) != null) {
                return true;
            }
        } catch (Exception e) {
        }
        return false;
    }

    private static <T> List<T> loadOSGi(Class<T> serviceClazz) {
        // TODO: improve logging
        Object bundle = osgi.getBundle(serviceClazz);
        System.out.println("Found bundle " + bundle);
        osgi.start(bundle);
        Object bundleContext = osgi.getBundleContext(bundle);
        Object[] bundles = osgi.getBundles(bundleContext);
        System.out.println("Found bundle " + bundleContext);
        System.out.println("Found bundles " + bundles);
        Map<Object, List<String>> bundleToServiceClassnamesMap = new HashMap<>();
        List<T> providers = new ArrayList<>();
        for (Object providerBundle : bundles) {
            URL url = osgi.getEntry(providerBundle, "META-INF/services/" + serviceClazz.getName());
            if (url != null) {
                System.out.println("Found " + url + " in " + providerBundle);
                List<String> classnames = readAllLines(url);
                if (classnames != null) {
                    bundleToServiceClassnamesMap.put(providerBundle, classnames);
                    System.out.println("Providers " + classnames);
                    for (String classname : classnames) {
                        Class<?> providerClass = osgi.loadClass(providerBundle, classname);
                        if (providerClass != null) {
                            try {
                                T provider = serviceClazz.cast(providerClass.newInstance());
                                System.out.println("Provider instance " + provider);
                                providers.add(provider);
                            } catch (InstantiationException | IllegalAccessException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
        return providers;
    }

    private static OSGiReflection osgi = new OSGiReflection();

    private static class OSGiReflection {

        private final Class<?> frameworkUtilClass = loadClass("org.osgi.framework.FrameworkUtil");
        private final Class<?> bundleClass = loadClass("org.osgi.framework.Bundle");
        private final Class<?> bundleContextClass = loadClass("org.osgi.framework.BundleContext");
        private final Method getBundleMethod = loadMethod(frameworkUtilClass, "getBundle", Class.class);
        private final Method startMethod = loadMethod(bundleClass, "start");
        private final Method getBundleContextMethod = loadMethod(bundleClass, "getBundleContext");
        private final Method getBundlesMethod = loadMethod(bundleContextClass, "getBundles");
        private final Method loadClassMethod = loadMethod(bundleClass, "loadClass", String.class);
        private final Method getEntryMethod = loadMethod(bundleClass, "getEntry", String.class);
        private final boolean osgiEnabled;

        public OSGiReflection() {
            this.osgiEnabled = false;
        }



        private Class<?> loadClass(String name) {
            try {
                return Class.forName(name);
            } catch (ClassNotFoundException ex) {
                return null;
            }
        }

        private Method loadMethod(Class<?> clazz, String name, Class<?>... parameterTypes) {
            if (clazz == null) {
                return null;
            }

            try {
                return clazz.getMethod(name, parameterTypes);
            } catch (NoSuchMethodException | SecurityException ex) {
                Logger.getLogger(ServiceLoaderOSGiWrapper.class.getName()).log(Level.SEVERE, null, ex);
                return null;
            }
        }

        private Object getBundle(Object classFromBundle) {
            try {
                return getBundleMethod.invoke(null, classFromBundle);
            } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                Logger.getLogger(ServiceLoaderOSGiWrapper.class.getName()).log(Level.SEVERE, null, ex);
            }
            return null;
        }

        private void start(Object bundle) {
            try {
                startMethod.invoke(bundle);
            } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                Logger.getLogger(ServiceLoaderOSGiWrapper.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        private Object getBundleContext(Object bundle) {
            try {
                return getBundleContextMethod.invoke(bundle);
            } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                Logger.getLogger(ServiceLoaderOSGiWrapper.class.getName()).log(Level.SEVERE, null, ex);
            }
            return null;
        }

        private Object[] getBundles(Object bundleContext) {
            try {
                return (Object[]) getBundlesMethod.invoke(bundleContext);
            } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                Logger.getLogger(ServiceLoaderOSGiWrapper.class.getName()).log(Level.SEVERE, null, ex);
            }
            return null;
        }

        private URL getEntry(Object bundle, String name) {
            try {
                return (URL) getEntryMethod.invoke(bundle, name);
            } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                Logger.getLogger(ServiceLoaderOSGiWrapper.class.getName()).log(Level.SEVERE, null, ex);
            }
            return null;
        }

        private Class<?> loadClass(Object bundle, String name) {
            try {
                return (Class<?>) loadClassMethod.invoke(bundle, name);
            } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                Logger.getLogger(ServiceLoaderOSGiWrapper.class.getName()).log(Level.SEVERE, null, ex);
            }
            return null;
        }
    }

    private static List<String> readAllLines(URL url) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"))) {
            List<String> result = new ArrayList<>();
            for (;;) {
                String line = reader.readLine();
                if (line == null) {
                    break;
                }
                result.add(line);
            }
            return result;
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }

}
