/**
 * 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.tomee.loader;

import org.apache.openejb.config.NewLoaderLogic;
import org.apache.openejb.util.URLs;
import org.apache.tomcat.JarScanFilter;
import org.apache.tomcat.JarScanType;
import org.apache.tomcat.JarScannerCallback;
import org.apache.tomcat.util.scan.StandardJarScanner;
import org.apache.xbean.finder.ClassLoaders;
import org.apache.xbean.finder.filter.Filter;
import org.apache.xbean.finder.filter.Filters;

import javax.servlet.ServletContext;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

import static java.lang.ClassLoader.getSystemClassLoader;

// todo: share common tld parsing, tomcat has a built in method for it, ensure we reuse it
public class TomEEJarScanner extends StandardJarScanner {
    private static final TomEEFilter DEFAULT_JAR_SCAN_FILTER = new TomEEFilter(null);

    public TomEEJarScanner() {
        setJarScanFilter(DEFAULT_JAR_SCAN_FILTER);
    }

    private void configureFilter(final JarScanFilter jarScanFilter) {
        setJarScanFilter(new TomEEFilter(jarScanFilter));
    }

    @Override
    public void setJarScanFilter(final JarScanFilter jarScanFilter) {
        super.setJarScanFilter(jarScanFilter);
        if (!TomEEFilter.class.isInstance(jarScanFilter)) {
            configureFilter(jarScanFilter);
        }
    }

    @Override
    public void scan(final JarScanType scanType, final ServletContext context, final JarScannerCallback callback) {
        super.scan(scanType, context, callback);
        if (!embeddedSurefireScanning(scanType, context, callback) && isScanClassPath() && !URLClassLoader.class.isInstance(getSystemClassLoader())
                && !Boolean.getBoolean("tomee.classpath.scanning.disabled")) {
            // TODO: check on tomcat upgrade if it is fixed
            final String cp = System.getProperty("java.class.path");
            final Collection<URL> urls = new HashSet<>();
            for (final String jar : cp.split(File.pathSeparator)) {
                try {
                    urls.add(new File(jar).toURI().toURL());
                } catch (MalformedURLException e) {
                    // no-op
                }
            }
            doScan(scanType, callback, new LinkedList<>(urls));
        }
    }

    private boolean embeddedSurefireScanning(final JarScanType scanType, final ServletContext context, final JarScannerCallback callback) {
        if (isScanClassPath() && System.getProperty("surefire.real.class.path") != null) {
            try {
                final Set<URL> urls = ClassLoaders.findUrls(context.getClassLoader().getParent());
                doScan(scanType, callback, new LinkedList<>(urls));
                return true;
            } catch (final IOException e) {
                // no-op
            }
        }
        return false;
    }

    private void doScan(final JarScanType scanType, final JarScannerCallback callback, final Deque<URL> urls) {
        Method process = null;
        final boolean scanAllDirectories = isScanAllDirectories();
        for (final URL url : urls) {
            final File cpe = URLs.toFile(url);
            if ((cpe.getName().endsWith(".jar") ||
                    scanType == JarScanType.PLUGGABILITY ||
                    scanAllDirectories) &&
                    getJarScanFilter().check(scanType, cpe.getName())) {
                try {
                    if (process == null) {
                        process = StandardJarScanner.class.getDeclaredMethod("process",
                                JarScanType.class, JarScannerCallback.class, URL.class, String.class, boolean.class, Deque.class);
                        if (!process.isAccessible()) {
                            process.setAccessible(true);
                        }
                    }
                    process.invoke(this, scanType, callback, url, null, true, urls);
                } catch (final Exception ioe) {
                    // no-op
                }
            }
        }
    }

    public /*context.xml*/ static class TomEEFilter implements JarScanFilter {
        private static final Filter INCLUDE = Filters.tokens("javax.faces-2.", "spring-security-taglibs", "spring-webmvc");
        private final JarScanFilter delegate;

        public TomEEFilter() {
            this(null);
        }

        public TomEEFilter(final JarScanFilter jarScanFilter) {
            this.delegate = jarScanFilter;
        }

        @Override
        public boolean check(final JarScanType jarScanType, final String jarName) {
            if (jarScanType == JarScanType.TLD) {
                if (INCLUDE.accept(jarName)) {
                    return true;
                }
                if (jarName.startsWith("tomcat-websocket") || jarName.startsWith("myfaces-impl") /* see org.apache.tomee.jasper.TomEETldScanner.scanPlatform */) {
                    return false;
                }
            }
            if (jarName.startsWith("johnzon-") || jarName.startsWith("xx-arquillian-tomee-")) {
                return false; // but we scan it in openejb scanning
            }
            return !NewLoaderLogic.skip(jarName) && (delegate == null || delegate.check(jarScanType, jarName));
        }

        public JarScanFilter getDelegate() {
            return delegate;
        }
    }
}
