/*
 * 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.openejb.config;

import org.apache.openejb.core.EmptyResourcesClassLoader;
import org.apache.openejb.loader.SystemInstance;
import org.apache.xbean.finder.archive.Archive;
import org.apache.xbean.finder.archive.ClassesArchive;
import org.apache.xbean.finder.archive.ClasspathArchive;
import org.apache.xbean.finder.archive.CompositeArchive;
import org.apache.xbean.finder.archive.FilteredArchive;
import org.apache.xbean.finder.filter.Filter;
import org.apache.xbean.finder.filter.FilterList;
import org.apache.xbean.finder.filter.PackageFilter;

import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class ConfigurableClasspathArchive extends CompositeArchive implements ScanConstants {
    private static final Method CLOSE_LOADER;
    static {
        Method m = null;
        try {
            m = URLClassLoader.class.getMethod("close");
        } catch (final Throwable e) {
            // no-op
        }
        CLOSE_LOADER = m;
    }
    public ConfigurableClasspathArchive(final Module module, final URL... urls) {
        this(module, Arrays.asList(urls));
    }

    public ConfigurableClasspathArchive(final Module module, final Iterable<URL> urls) {
        this(module, false, urls);
    }

    public ConfigurableClasspathArchive(final ClassLoader loader, final Iterable<URL> urls) {
        this(loader, false, urls);
    }

    public ConfigurableClasspathArchive(final ClassLoader loader, final boolean forceDescriptor, final Iterable<URL> urls) {
        this(new FakeModule(loader), forceDescriptor, urls);
    }

    public ConfigurableClasspathArchive(final ClassLoader loader, final URL url) {
        this(new FakeModule(loader), Arrays.asList(url));
    }

    public ConfigurableClasspathArchive(final Module module, final boolean forceDescriptor, final Iterable<URL> urls) {
        super(archive(module, urls, forceDescriptor));
    }

    public static List<Archive> archive(final Module module, final Iterable<URL> urls, final boolean forceDescriptor) {
        final List<Archive> archives = new ArrayList<Archive>();
        for (final URL location : urls) {
            try {
                archives.add(archive(module, location, forceDescriptor));
            } catch (final Exception e) {
                // ignored
            }
        }
        return archives;
    }

    public static Archive archive(final Module module, final URL location, final boolean forceDescriptor) {
        final ClassLoader loader = module.getClassLoader();
        final String name = "META-INF/" + name();
        try {
            final URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{location}, new EmptyResourcesClassLoader());
            final URL scanXml = urlClassLoader.getResource(name);
            if (CLOSE_LOADER != null) {
                try {
                    CLOSE_LOADER.invoke(urlClassLoader);
                } catch (final Throwable ignored) {
                    // no-op
                }
            }

            if (scanXml == null && !forceDescriptor) {
                return ClasspathArchive.archive(loader, location);
            } else if (scanXml == null) {
                return new ClassesArchive();
            }

            // read descriptors
            final ScanUtil.ScanHandler scan = ScanUtil.read(scanXml);
            final Archive packageArchive = packageArchive(scan.getPackages(), loader, location);
            final Archive classesArchive = classesArchive(scan.getPackages(), scan.getClasses(), loader);

            if (packageArchive != null && classesArchive != null) {
                return new CompositeArchive(classesArchive, packageArchive);
            } else if (packageArchive != null) {
                return packageArchive;
            }
            return classesArchive;
        } catch (final IOException e) {
            if (forceDescriptor) {
                return new ClassesArchive();
            }
            return ClasspathArchive.archive(loader, location);
        }
    }

    private static String name() {
        return SystemInstance.get().getProperty(SCAN_XML_PROPERTY, SCAN_XML_NAME);
    }

    public static Archive packageArchive(final Set<String> packageNames, final ClassLoader loader, final URL url) {
        if (!packageNames.isEmpty()) {
            return new FilteredArchive(ClasspathArchive.archive(loader, url), filters(packageNames));
        }
        return null;
    }

    private static Filter filters(final Set<String> packageNames) {
        final List<Filter> filters = new ArrayList<Filter>();
        for (final String packageName : packageNames) {
            filters.add(new PackageFilter(packageName));
        }
        return new FilterList(filters);
    }

    public static Archive classesArchive(final Set<String> packages, final Set<String> classnames, final ClassLoader loader) {
        Class<?>[] classes = new Class<?>[classnames.size()];
        int i = 0;
        for (final String clazz : classnames) {
            // skip classes managed by package filtering
            if (packages != null && clazzInPackage(packages, clazz)) {
                continue;
            }

            try {
                classes[i++] = loader.loadClass(clazz);
            } catch (final ClassNotFoundException e) {
                // ignored
            }
        }

        if (i != classes.length) { // shouldn't occur
            final Class<?>[] updatedClasses = new Class<?>[i];
            System.arraycopy(classes, 0, updatedClasses, 0, i);
            classes = updatedClasses;
        }

        return new ClassesArchive(classes);
    }

    private static boolean clazzInPackage(final Collection<String> packagename, final String clazz) {
        for (final String str : packagename) {
            if (clazz.startsWith(str)) {
                return true;
            }
        }
        return false;
    }

    public static class FakeModule extends Module {
        public FakeModule(final ClassLoader loader) {
            this(loader, null);
        }

        public FakeModule(final ClassLoader loader, final Map<String, Object> altDD) {
            this(loader, altDD, name());
        }

        public FakeModule(final ClassLoader loader, final Map<String, Object> altDD, final String name) {
            super(false);
            setClassLoader(loader);

            final URL scanXml;
            if (altDD == null) {
                scanXml = loader.getResource("META-INF/" + name);
            } else {
                scanXml = (URL) altDD.get(name);
            }
            if (scanXml != null) {
                getAltDDs().put(SCAN_XML_NAME, scanXml);
            }
        }
    }
}
