/*
 * 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.openide.util.lookup;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openide.util.Lookup;
import org.openide.util.lookup.implspi.SharedClassObjectBridge;

/**
 * @author Jaroslav Tulach, Jesse Glick
 * @see Lookups#metaInfServices(ClassLoader,String)
 * @see "#14722"
 */
final class MetaInfServicesLookup extends AbstractLookup {
    static final Logger LOGGER = Logger.getLogger(MetaInfServicesLookup.class.getName());
    private static final MetaInfCache CACHE = new MetaInfCache(512);
    private static Reference<Executor> RP = new WeakReference<Executor>(null);
    static synchronized Executor getRP() {
        Executor res = RP.get();
        if (res == null) {
            try {
                Class<?> seek = Class.forName("org.openide.util.RequestProcessor");
                res = (Executor)seek.newInstance();
            } catch (Throwable t) {
                try {
                    res = Executors.newSingleThreadExecutor();
                } catch (Throwable t2) {
                    res = new Executor() {
                        @Override
                        public void execute(Runnable command) {
                            command.run();
                        }
                    };
                }
            }
            RP = new SoftReference<Executor>(res);
        }
        return res;
    }

    /** A set of all requested classes.
     * Note that classes that we actually succeeded on can never be removed
     * from here because we hold a strong reference to the loader.
     * However we also hold classes which are definitely not loadable by
     * our loader.
     */
    private final Map<Class<?>,Object> classes = new WeakHashMap<Class<?>,Object>();

    /** class loader to use */
    private final ClassLoader loader;
    /** prefix to prepend */
    private final String prefix;

    /** Create a lookup reading from a specified classloader.
     */
    @SuppressWarnings("LeakingThisInConstructor")
    public MetaInfServicesLookup(ClassLoader loader, String prefix) {
        this.loader = loader;
        this.prefix = prefix;

        LOGGER.log(Level.FINE, "Created: {0}", this);
    }

    @Override
    public String toString() {
        return "MetaInfServicesLookup[" + loader + "]"; // NOI18N
    }

    /** Initialize soon, before result's listeners are activated
     */
    @Override
    void beforeLookupResult(Template<?> template) {
        beforeLookup(template);
    }

    /* Tries to load appropriate resources from manifest files.
     */
    @Override
    protected final void beforeLookup(Lookup.Template<?> t) {
        Class<?> c = t.getType();

        Collection<AbstractLookup.Pair<?>> toAdd = null;
        synchronized (this) {
            if (classes.get(c) == null) { // NOI18N
                toAdd = new ArrayList<Pair<?>>();
            } else {
                // ok, nothing needs to be done
                return;
            }
        }
        if (toAdd != null) {
            Set<Class<?>> all = new HashSet<Class<?>>();
            for (Class type : allSuper(c, all)) {
                search(type, toAdd);
            }
        }
        HashSet<R> listeners = null;
        synchronized (this) {
            if (classes.put(c, "") == null) { // NOI18N
                // Added new class, search for it.
                LinkedHashSet<AbstractLookup.Pair<?>> lhs = getPairsAsLHS();
                List<Item> arr = new ArrayList<Item>();
                for (Pair<?> lh : lhs) {
                    arr.add((Item)lh);
                }
                for (Pair<?> p : toAdd) {
                    insertItem((Item) p, arr);
                }
                listeners = setPairsAndCollectListeners(arr);
            }
        }
        if (listeners != null) {
            notifyIn(getRP(), listeners);
        }
    }
    
    private Set<Class<?>> allSuper(Class<?> clazz, Set<Class<?>> all) {
        all.add(clazz);
        Class<?> sup = clazz.getSuperclass();
        if (sup != null && sup != Object.class) {
            all.add(sup);
        }
        for (Class<?> c : clazz.getInterfaces()) {
            allSuper(c, all);
        }
        return all;
    }

    /** Finds all pairs and adds them to the collection.
     *
     * @param clazz class to find
     * @param result collection to add Pair to
     */
    private void search(Class<?> clazz, Collection<AbstractLookup.Pair<?>> result) {
        if (LOGGER.isLoggable(Level.FINER)) {
            LOGGER.log(Level.FINER, "Searching for {0} in {1} from {2}", new Object[] {clazz.getName(), clazz.getClassLoader(), this});
        }

        String res = prefix + clazz.getName(); // NOI18N
        Enumeration<URL> en;

        try {
            en = loader.getResources(res);
        } catch (IOException ioe) {
            // do not use ErrorManager because we are in the startup code
            // and ErrorManager might not be ready
            ioe.printStackTrace();

            return;
        }

        // Do not create multiple instances in case more than one JAR
        // has the same entry in it (and they load to the same class).
        // Probably would not happen, assuming JARs only list classes
        // they own, but just in case...
        List<Item> foundClasses = new ArrayList<Item>();
        Collection<Class<?>> removeClasses = new ArrayList<Class<?>>();

        boolean foundOne = false;

        while (en.hasMoreElements()) {
            if (!foundOne) {
                foundOne = true;

                // Double-check that in fact we can load the *interface* class.
                // For example, say class I is defined in two JARs, J1 and J2.
                // There is also an implementation M1 defined in J1, and another
                // implementation M2 defined in J2.
                // Classloaders C1 and C2 are made from J1 and J2.
                // A MetaInfServicesLookup is made from C1. Then the user asks to
                // lookup I as loaded from C2. J1 has the services line and lists
                // M1, and we can in fact make it. However it is not of the desired
                // type to be looked up. Don't do this check, which could be expensive,
                // unless we expect to be getting some results, however.
                Class<?> realMcCoy = null;

                try {
                    realMcCoy = loader.loadClass(clazz.getName());
                } catch (ClassNotFoundException cnfe) {
                    // our loader does not know about it, OK
                }

                if (realMcCoy != clazz) {
                    // Either the interface class is not available at all in our loader,
                    // or it is not the same version as we expected. Don't provide results.
                    if (realMcCoy != null) {
                        LOGGER.log(Level.WARNING, "{0} is not the real McCoy! Actually found it in {1} (from {2}) but searched for from {3}",
                                new Object[] {clazz.getName(), realMcCoy.getClassLoader(), loader, clazz.getClassLoader()}); // NOI18N
                    } else {
                        LOGGER.log(Level.WARNING, "{0} could not be found in {1}", new Object[] {clazz.getName(), loader}); // NOI18N
                    }

                    return;
                }
            }

            URL url = en.nextElement();
            Item currentItem = null;

            try {
                InputStream is = url.openStream();

                try {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); // NOI18N

                    // XXX consider using ServiceLoaderLine instead
                    while (true) {
                        String line = reader.readLine();

                        if (line == null) {
                            break;
                        }

                        line = line.trim();

                        // is it position attribute?
                        if (line.startsWith("#position=")) {
                            if (currentItem == null) {
                                LOGGER.log(Level.INFO, "Found line ''{0}'' in {1} but there is no item to associate it with", new Object[] {line, url});
                                continue;
                            }

                            try {
                                currentItem.position = Integer.parseInt(line.substring(10));
                            } catch (NumberFormatException e) {
                                // do not use ErrorManager because we are in the startup code
                                // and ErrorManager might not be ready
                                e.printStackTrace();
                            }
                        }

                        if (currentItem != null) {
                            insertItem(currentItem, foundClasses);
                            currentItem = null;
                        }

                        // Ignore blank lines and comments.
                        if (line.length() == 0) {
                            continue;
                        }

                        boolean remove = false;

                        if (line.charAt(0) == '#') {
                            if ((line.length() == 1) || (line.charAt(1) != '-')) {
                                continue;
                            }

                            // line starting with #- is a sign to remove that class from lookup
                            remove = true;
                            line = line.substring(2);
                        }
                        Class<?> inst = null;
                        try {
                            Object ldr = url.getContent(new Class[] { ClassLoader.class });
                            if (ldr instanceof ClassLoader) {
                                inst = Class.forName(line, false, (ClassLoader)ldr);
                            }
                        } catch (LinkageError err) {
                            // go on
                        } catch (ClassNotFoundException ex) {
                            LOGGER.log(Level.FINER, "No class found in " + url, ex);
                        } catch (IOException ex) {
                            LOGGER.log(Level.FINER, "URL does not support classloader protocol " + url, ex);
                        }
                        
                        if (inst == null) try {
                            // Most lines are fully-qualified class names.
                            inst = Class.forName(line, false, loader);
                        } catch (LinkageError err) {
                            if (remove) {
                                continue;
                            }
                            throw new ClassNotFoundException(err.getMessage(), err);
                        } catch (ClassNotFoundException cnfe) {
                            if (remove) {
                                // if we are removing somthing and the something
                                // cannot be found it is ok to do nothing
                                continue;
                            } else {
                                // but if we are not removing just rethrow
                                throw cnfe;
                            }
                        }

                        if (!clazz.isAssignableFrom(inst)) {
                            throw new ClassNotFoundException(clazzToString(inst) + " not a subclass of " + clazzToString(clazz)); // NOI18N
                        }

                        if (remove) {
                            removeClasses.add(inst);
                        } else {
                            // create new item here, but do not put it into
                            // foundClasses array yet because following line
                            // might specify its position
                            currentItem = new Item(inst);
                        }
                    }

                    if (currentItem != null) {
                        insertItem(currentItem, foundClasses);
                        currentItem = null;
                    }
                } finally {
                    is.close();
                }
            } catch (ClassNotFoundException ex) {
                LOGGER.log(Level.INFO, null, ex);
            } catch (IOException ex) {
                LOGGER.log(Level.INFO, null, ex);
            }
        }

        LOGGER.log(Level.FINER, "Found impls of {0}: {1} and removed: {2} from: {3}", new Object[] {clazz.getName(), foundClasses, removeClasses, this});

        /* XXX makes no sense, wrong types:
        foundClasses.removeAll(removeClasses);
         */

        for (Item item : foundClasses) {
            if (removeClasses.contains(item.clazz())) {
                continue;
            }

            result.add(item);
        }
    }
    private static String clazzToString(Class<?> clazz) {
        String loc = null;
        try {
            if (clazz.getProtectionDomain() != null && clazz.getProtectionDomain().getCodeSource() != null) {
                loc = clazz.getProtectionDomain().getCodeSource().getLocation().toString();
            }
        } catch (Throwable ex) {
            loc = ex.getMessage();
        }
        return clazz.getName() + "@" + clazz.getClassLoader() + ":" + loc; // NOI18N
    }

    /**
     * Insert item to the list according to item.position value.
     */
    private void insertItem(Item item, List<Item> list) {
        // no position? -> add it to the end
        if (item.position == -1) {
            if (!list.contains(item)) {
                list.add(item);
            }

            return;
        }

        int foundIndex = -1;
        int index = -1;
        for (Item i : list) {
            if (i.equals(item)) {
                return;
            }
            index++;

            if (foundIndex < 0) {
                if (i.position == -1 || i.position > item.position) {
                    foundIndex = index;
                }
            }
        }
        if (foundIndex < 0) {
            list.add(item);             // add to the end
        } else {
            list.add(foundIndex, item); // insert at found index
        }
    }

    static Item createPair(Class<?> clazz) {
        return new Item(clazz);
    }

    /** Pair that holds name of a class and maybe the instance.
     */
    private static final class Item extends AbstractLookup.Pair<Object> {
        /** May be one of three things:
         * 1. The implementation class which was named in the services file.
         * 2. An instance of it.
         * 3. Null, if creation of the instance resulted in an error.
         */
        private Object object;
        private int position = -1;

        public Item(Class<?> clazz) {
            this.object = clazz;
        }

        @Override
        public String toString() {
            return "MetaInfServicesLookup.Item[" + clazz().getName() + "]"; // NOI18N
        }
        
        /** Finds the class.
         */
        private Class<? extends Object> clazz() {
            Object o = object;
            if (o instanceof CantInstantiate) {
                return ((CantInstantiate)o).clazz;
            }
            if (o instanceof Class<?>) {
                return (Class<? extends Object>) o;
            } else if (o != null) {
                return o.getClass();
            } else {
                // Broken.
                return Object.class;
            }
        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof Item) {
                return ((Item) o).clazz().equals(clazz());
            }

            return false;
        }

        @Override
        public int hashCode() {
            return clazz().hashCode();
        }

        protected @Override boolean instanceOf(Class<?> c) {
            return c.isAssignableFrom(clazz());
        }

        public @Override Class<?> getType() {
            return clazz();
        }

        public @Override Object getInstance() {
            Object o = object; // keeping local copy to avoid another
            if (o instanceof CantInstantiate) {
                return null;
            }

            // thread to modify it under my hands
            if (o instanceof Class<?>) {
                synchronized (o) { // o is Class and we will not create 
                                   // 2 instances of the same class
                    Class<?> c = ((Class<?>) o);
                    try {
                        o = CACHE.findInstance(c);

                        if (o == null) {
                            o = SharedClassObjectBridge.newInstance(c);
                            // if the instance was created during newInstance call
                            // and returned, return always the 1st instance, so 
                            // only a single instance is ever observable outside Lookup.
                            Object other = CACHE.findInstance(c);
                            if (other != null) {
                                return object = other;
                            }
                            CACHE.storeInstance(o);
                        }

                        // Do not assign to instance var unless there is a complete synch
                        // block between the newInstance and this line. Otherwise we could
                        // be assigning a half-constructed instance that another thread
                        // could see and return immediately.
                        object = o;
                    } catch (Exception ex) {
                        LOGGER.log(Level.INFO, "Cannot create " + object, ex);
                        object = new CantInstantiate(c);
                        return null;
                    } catch (LinkageError x) { // #174055 + NoClassDefFoundError
                        LOGGER.log(Level.FINE, "Cannot create " + object, x); //NOI18N
                        object = new CantInstantiate(c);
                        return null;
                    }
                }
            }

            return object;
        }

        public @Override String getDisplayName() {
            return clazz().getName();
        }

        public @Override String getId() {
            return clazz().getName();
        }

        protected @Override boolean creatorOf(Object obj) {
            return obj == object;
        }

    }
    private static final class CantInstantiate {
        final Class<?> clazz;

        public CantInstantiate(Class<?> clazz) {
            assert clazz != null;
            this.clazz = clazz;
        }
    }
}
