/*
 * 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.camel.quarkus.core;

import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Stream;

import io.quarkus.runtime.ImageMode;
import org.apache.camel.spi.PackageScanResourceResolver;
import org.apache.camel.spi.Resource;
import org.apache.camel.spi.ResourceLoader;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.ResourceHelper;
import org.apache.camel.support.scan.DefaultPackageScanResourceResolver;
import org.apache.camel.util.AntPathMatcher;
import org.apache.camel.util.ObjectHelper;
import org.jboss.logging.Logger;

/**
 * Custom {@link PackageScanResourceResolver} that adds the specific {@link ClassLoader} instance
 * that Camel Quarkus requires for resolving resources. Also performs native image capable classpath glob pattern
 * resource resolution.
 */
public class CamelQuarkusPackageScanResourceResolver extends DefaultPackageScanResourceResolver {
    private static final Logger LOG = Logger.getLogger(CamelQuarkusPackageScanResourceResolver.class);
    private static final URI NATIVE_IMAGE_FILESYSTEM_URI = URI.create("resource:/");
    private FileSystem resourceFileSystem;

    @Override
    public void initialize() {
        addClassLoader(Thread.currentThread().getContextClassLoader());
    }

    @Override
    protected void doStop() throws Exception {
        if (resourceFileSystem != null) {
            resourceFileSystem.close();
            resourceFileSystem = null;
        }
    }

    @Override
    public Collection<Resource> findResources(String location) throws Exception {
        Collection<Resource> resources = super.findResources(location);

        // If no matches were found for the location pattern in native mode, try to use the resource scheme filesystem
        if (resources.isEmpty() && ImageMode.current().isNativeImage()) {
            String scheme = ResourceHelper.getScheme(location);
            if (isClassPathPattern(location, scheme)) {
                FileSystem fileSystem = getNativeImageResourceFileSystem();
                ResourceLoader resourceLoader = PluginHelper.getResourceLoader(getCamelContext());
                String root = AntPathMatcher.INSTANCE.determineRootDir(location);
                String rootWithoutScheme = scheme != null ? root.substring(scheme.length()) : root;
                String subPattern = location.substring(root.length());
                Path startPath = fileSystem.getPath(rootWithoutScheme);

                if (!Files.exists(startPath)) {
                    LOG.tracef("Failed to find resources for location: %s as path %s does not exist", location, startPath);
                    return resources;
                }

                LOG.tracef("Finding native resources for location: %s, sub pattern %s, under path %s", location, subPattern,
                        rootWithoutScheme);

                // Iterate classpath resources in the native application and try to find matches
                Files.walkFileTree(startPath, new SimpleFileVisitor<>() {
                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                        Path pathToMatch = file.getNameCount() > 1 ? file.subpath(1, file.getNameCount()) : file.getFileName();

                        LOG.tracef("Checking for native resource match for: %s", pathToMatch);
                        if (AntPathMatcher.INSTANCE.match(subPattern, pathToMatch.toString())) {
                            LOG.tracef("Matched native resource: %s with pattern: %s", file, subPattern);
                            resources.add(resourceLoader.resolveResource(file.toString()));
                        }
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult visitFileFailed(Path file, IOException exc) {
                        LOG.tracef(exc, "Failed to process resource: %s", file);
                        return FileVisitResult.CONTINUE;
                    }
                });

                // Fallback on matching files explicitly in the resolved directory path
                if (resources.isEmpty() && Files.isDirectory(startPath)) {
                    try (Stream<Path> files = Files.list(startPath)) {
                        files.map(Path::getFileName)
                                .map(Path::toString)
                                .filter(path -> AntPathMatcher.INSTANCE.match(subPattern, path))
                                .map(path -> startPath.resolve(path).toString())
                                .peek(path -> LOG.tracef("Matched native resource: %s with pattern: %s", path, subPattern))
                                .map(resourceLoader::resolveResource)
                                .forEach(resources::add);
                    }
                }
            }
        }

        return resources;
    }

    protected boolean isClassPathPattern(String location, String scheme) {
        if (ObjectHelper.isNotEmpty(location)) {
            return (AntPathMatcher.INSTANCE.isPattern(location))
                    && (ObjectHelper.isEmpty(scheme) || "classpath:".equals(scheme));
        }
        return false;
    }

    protected FileSystem getNativeImageResourceFileSystem() {
        // Must lazy init the FileSystem at runtime so that resources are discoverable
        if (resourceFileSystem == null) {
            try {
                resourceFileSystem = FileSystems.newFileSystem(NATIVE_IMAGE_FILESYSTEM_URI,
                        Collections.singletonMap("create", "true"));
            } catch (FileSystemAlreadyExistsException ex) {
                resourceFileSystem = FileSystems.getFileSystem(NATIVE_IMAGE_FILESYSTEM_URI);
            } catch (IOException ex) {
                throw new IllegalStateException(ex);
            }
        }
        return resourceFileSystem;
    }
}
