/*
 * 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.meecrowave.openwebbeans;

import org.apache.meecrowave.Meecrowave;
import org.apache.meecrowave.logging.tomcat.LogFacade;
import org.apache.tomcat.JarScanFilter;
import org.apache.webbeans.config.WebBeansContext;
import org.apache.webbeans.corespi.scanner.xbean.CdiArchive;
import org.apache.webbeans.corespi.scanner.xbean.OwbAnnotationFinder;
import org.apache.webbeans.spi.BdaScannerService;
import org.apache.webbeans.spi.BeanArchiveService;
import org.apache.webbeans.util.WebBeansUtil;
import org.apache.webbeans.web.scanner.WebScannerService;
import org.apache.xbean.finder.AnnotationFinder;
import org.apache.xbean.finder.filter.Filter;

import javax.servlet.ServletContext;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;

import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toSet;
import static org.apache.tomcat.JarScanType.PLUGGABILITY;

public class OWBTomcatWebScannerService extends WebScannerService {
    private final LogFacade logger = new LogFacade(OWBTomcatWebScannerService.class.getName());
    private final BdaScannerService delegate;
    private final Supplier<OwbAnnotationFinder> finderAccessor;

    protected JarScanFilter filter;
    private String jreBase;

    // just for logging (== temp)
    private final Set<String> urls = new HashSet<>();
    private String docBase;
    private String shared;
    private Consumer<File> fileVisitor;

    public OWBTomcatWebScannerService() {
        this(null, null);
    }


    public OWBTomcatWebScannerService(final BdaScannerService delegate, final Supplier<OwbAnnotationFinder> finderAccessor) {
        this.delegate = delegate;
        this.finderAccessor = finderAccessor;
    }

    @Override
    public void init(final Object context) {
        if (delegate != null) {
            delegate.init(context);
        }
    }

    @Override
    public OwbAnnotationFinder getFinder() {
        if (finderAccessor != null) {
            return finderAccessor.get();
        }
        return super.getFinder();
    }

    @Override
    public Map<BeanArchiveService.BeanArchiveInformation, Set<Class<?>>> getBeanClassesPerBda() {
        if (delegate != null) {
            return delegate.getBeanClassesPerBda();
        }
        return super.getBeanClassesPerBda();
    }

    @Override
    public void release() {
        if (delegate != null) {
            delegate.release();
        } else {
            super.release();
        }
    }

    @Override
    public Set<Class<?>> getBeanClasses() {
        if (delegate != null) {
            return delegate.getBeanClasses();
        }
        return super.getBeanClasses();
    }

    @Override
    public void scan() {
        if (delegate != null) {
            if (getFinder() == null) {
                delegate.scan();
            }
            if (finder == null) {
                finder = getFinder();
            }
        }

        if (finder != null) {
            return;
        }

        super.scan();
        scanGroovy(WebBeansUtil.getCurrentClassLoader());
        if (!urls.isEmpty()) {
            logger.info("OpenWebBeans scanning:");
            final String m2 = new File(System.getProperty("user.home", "."), ".m2/repository").getAbsolutePath();
            final String base = ofNullable(docBase).orElse("$$$");
            final String sharedBase = ofNullable(shared).orElse("$$$");
            final String runnerBase = ofNullable(System.getProperty("meecrowave.base")).orElse("$$$");
            final String runnerHome = ofNullable(System.getProperty("meecrowave.home")).orElse("$$$");
            urls.stream().map(u -> {
                String shownValue = u
                        // protocol
                        .replace("file://", "")
                        .replace("file:", "")
                        .replace("jar:", "")
                        // URL suffix
                        .replace("!/META-INF/beans.xml", "")
                        .replace("!/", "")
                        // beans.xml
                        .replace("META-INF/beans.xml", "");

                // try to make it shorter
                if (shownValue.startsWith(m2)) {
                    shownValue = "${maven}/" + shownValue.substring(shownValue.replace(File.separatorChar, '/').lastIndexOf('/') + 1);
                } else if (shownValue.startsWith(base)) {
                    shownValue = "${app}" + shownValue.replace(base, "");
                } else if (shownValue.startsWith(sharedBase)) {
                    shownValue = "${shared}" + shownValue.replace(sharedBase, "");
                } else if (shownValue.startsWith(runnerBase)) {
                    shownValue = "${base}" + shownValue.replace(runnerBase, "");
                } else if (shownValue.startsWith(runnerHome)) {
                    shownValue = "${home}" + shownValue.replace(runnerHome, "");
                }

                return shownValue;
            }).sorted().forEach(v -> logger.info("    " + v));

            if (fileVisitor != null) {
                urls.stream()
                        .filter(this::isFile)
                        .map(this::toFile)
                        .filter(File::isDirectory)
                        .forEach(fileVisitor);
            }
        }
        urls.clear(); // no more needed
        filter = null;
        docBase = null;
        shared = null;
    }

    private File toFile(final String url) {
        try {
            return new File(new URL(url).getFile());
        } catch (final MalformedURLException e) {
            return new File(url.substring("file://".length(), url.length()));
        }
    }

    private boolean isFile(final String url) {
        return url.startsWith("file:") && !url.endsWith("!/") && !url.endsWith("!/META-INF/beans.xml");
    }

    private void scanGroovy(final ClassLoader currentClassLoader) {
        if (currentClassLoader == null || !currentClassLoader.getClass().getName().equals("groovy.lang.GroovyClassLoader")) {
            return;
        }
        try {
            final Class<?>[] getLoadedClasses = Class[].class.cast(
                    currentClassLoader.getClass()
                            .getMethod("getLoadedClasses")
                            .invoke(currentClassLoader));
            addClassesToDefault(getLoadedClasses);
        } catch (final Exception e) {
            new LogFacade(OWBTomcatWebScannerService.class.getName()).warn(e.getMessage());
        }
    }

    private void addClassesToDefault(final Class<?>[] all) throws Exception {
        if (all == null || all.length == 0) {
            return;
        }

        final Field linking = AnnotationFinder.class.getDeclaredField("linking");
        final Method readClassDef = AnnotationFinder.class.getDeclaredMethod("readClassDef", Class.class);
        if (!readClassDef.isAccessible()) {
            readClassDef.setAccessible(true);
        }
        if (!linking.isAccessible()) {
            linking.setAccessible(true);
        }

        final URI uri = URI.create("jar:file://!/"); // we'll never find it during scanning and it avoids to create a custom handler
        final URL url = uri.toURL();
        final String key = uri.toASCIIString();
        CdiArchive.FoundClasses foundClasses = archive.classesByUrl().get(key);
        if (foundClasses == null) {
            final BeanArchiveService beanArchiveService = webBeansContext().getBeanArchiveService();
            foundClasses = CdiArchive.FoundClasses.class.cast(
                    CdiArchive.FoundClasses.class.getConstructor(CdiArchive.class, URL.class, Collection.class, BeanArchiveService.BeanArchiveInformation.class)
                            .newInstance(null, url, new HashSet<>(), beanArchiveService.getBeanArchiveInformation(url)));
            archive.classesByUrl().put(key, foundClasses);
        }

        foundClasses.getClassNames().addAll(Stream.of(all).map(Class::getName).collect(toSet()));

        try {
            linking.set(finder, true);

            Stream.of(all).forEach(c -> { // populate classInfos map to support annotated mode which relies on ClassInfo
                try {
                    readClassDef.invoke(finder, c);
                } catch (final IllegalAccessException e) {
                    throw new IllegalStateException(e);
                } catch (final InvocationTargetException e) {
                    throw new IllegalStateException(e.getCause());
                }
            });
        } finally {
            try {
                linking.set(finder, false);
            } catch (final IllegalAccessException e) {
                // no-op
            }
        }
    }

    @Override
    protected void filterExcludedJars(final Set<URL> classPathUrls) {
        String jreBaseTmp;
        try {
            jreBaseTmp = new File(System.getProperty("java.home")).toURI().toURL().toExternalForm();
        } catch (MalformedURLException e) {
            jreBaseTmp = System.getProperty("java.home");
        }
        jreBase = jreBaseTmp;

        super.filterExcludedJars(classPathUrls);
    }

    @Override
    protected int isExcludedJar(final String path) {
        if (path.startsWith(jreBase) || (path.startsWith("jar:") && path.indexOf(jreBase) == 4)) {
            return jreBase.length();
        }

        // jar:file:spring-boot-cdi-launcher-1.0-SNAPSHOT.jar!/BOOT-INF/lib/x.jar!/
        if (path.startsWith("jar:file:") && path.endsWith(".jar!/")) {
            final int lastSep = path.substring(0, path.length() - 2).lastIndexOf('/');
            if (lastSep > 0) {
                return filter != null && filter.check(PLUGGABILITY, path.substring(lastSep + 1, path.length() - 2)) ?
                        -1 : (path.indexOf(".jar") - 1 /*should be lastIndexOf but filterExcludedJar logic would be broken*/);
            }
        }

        final int filenameIdx = path.replace(File.separatorChar, '/').replace("!/", "").lastIndexOf('/') + 1;
        if (filenameIdx < 0 || filenameIdx >= path.length()) { // unlikely
            return -1;
        }

        return filter!= null && filter.check(PLUGGABILITY, path.substring(filenameIdx)) ? -1 : (path.indexOf(".jar") - 1);
    }

    // replace init
    public void setFilter(final JarScanFilter filter, final ServletContext ctx) {
        this.filter = filter;

        super.init(ctx);
        final Meecrowave.Builder config = Meecrowave.Builder.class.cast(ServletContext.class.cast(ctx).getAttribute("meecrowave.configuration"));
        if (this.filter == null) {
            this.filter = new KnownJarsFilter(config);
        }

        final Filter userFilter = webBeansContext().getService(Filter.class);
        if (KnownClassesFilter.class.isInstance(userFilter)) {
            KnownClassesFilter.class.cast(userFilter).init(config);
        }
    }

    @Override
    protected void addWebBeansXmlLocation(final URL beanArchiveUrl) {
        final String url = beanArchiveUrl.toExternalForm();
        if (urls.add(of(url)
                .map(s -> s.startsWith("jar:") && s.endsWith("!/META-INF/beans.xml") ? s.substring("jar:".length(), s.length() - "!/META-INF/beans.xml".length()) : s)
                .get())) {
            super.doAddWebBeansXmlLocation(beanArchiveUrl);
        }
    }

    public void setShared(final String shared) {
        this.shared = ofNullable(shared).map(File::new).filter(File::isDirectory).map(File::getAbsolutePath).orElse(null);
    }

    public void setDocBase(final String docBase) {
        this.docBase = docBase;
    }

    public void setFileVisitor(final Consumer<File> fileVisitor) {
        this.fileVisitor = fileVisitor;
    }

    //don't rename this method - in case of owb2 it overrides the method defined in AbstractMetaDataDiscovery
    protected WebBeansContext webBeansContext() {
        return WebBeansContext.getInstance(); //only way to be compatible with owb 1.7.x and 2.x (without reflection)
    }
}
