/*
 *  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
 *
 *    https://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.grails.io.support;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import grails.util.Environment;

/**
 * Used to locate resources at build / development time
 *
 * @author Graeme Rocher
 * @since 3.0
 */
public class ResourceLocator {
    public static final String WILDCARD = "*";
    public static final String FILE_SEPARATOR = File.separator;
    public static final String CLOSURE_MARKER = "$";
    public static final String WEB_APP_DIR = "web-app";

    protected static final Resource NULL_RESOURCE = new ByteArrayResource("null".getBytes());

    protected PathMatchingResourcePatternResolver patchMatchingResolver;
    protected List<String> classSearchDirectories = new ArrayList<>();
    protected List<String> resourceSearchDirectories = new ArrayList<>();
    protected Map<String, Resource> classNameToResourceCache = new ConcurrentHashMap<>();
    protected Map<String, Resource> uriToResourceCache = new ConcurrentHashMap<>();
    protected ResourceLoader defaultResourceLoader = new FileSystemResourceLoader();
    protected boolean warDeployed = Environment.isWarDeployed();

    public void setSearchLocation(String searchLocation) {
        ResourceLoader resourceLoader = getDefaultResourceLoader();
        patchMatchingResolver = new PathMatchingResourcePatternResolver(resourceLoader);
        initializeForSearchLocation(searchLocation);
    }

    protected ResourceLoader getDefaultResourceLoader() {
        return defaultResourceLoader;
    }

    public void setSearchLocations(Collection<String> searchLocations) {
        patchMatchingResolver = new PathMatchingResourcePatternResolver(getDefaultResourceLoader());
        for (String searchLocation : searchLocations) {
            initializeForSearchLocation(searchLocation);
        }
    }

    private void initializeForSearchLocation(String searchLocation) {
        String searchLocationPlusSlash = searchLocation.endsWith("/") ? searchLocation : searchLocation + FILE_SEPARATOR;
        try {
            File[] directories = new File(searchLocationPlusSlash + GrailsResourceUtils.GRAILS_APP_DIR).listFiles(new FileFilter() {
                public boolean accept(File file) {
                    return file.isDirectory() && !file.isHidden();
                }
            });
            if (directories != null) {
                for (File directory : directories) {
                    classSearchDirectories.add(directory.getCanonicalPath());
                }
            }
        } catch (IOException e) {
            // ignore
        }

        classSearchDirectories.add(searchLocationPlusSlash + "src/main/java");
        classSearchDirectories.add(searchLocationPlusSlash + "src/main/groovy");
        resourceSearchDirectories.add(searchLocationPlusSlash);
    }

    public Resource findResourceForURI(String uri) {
        Resource resource = uriToResourceCache.get(uri);
        if (resource == null) {

            PluginResourceInfo info = inferPluginNameFromURI(uri);
            if (warDeployed) {
                Resource defaultResource = defaultResourceLoader.getResource(uri);
                if (defaultResource != null && defaultResource.exists()) {
                    resource = defaultResource;
                }
            }
            else {
                String uriWebAppRelative = WEB_APP_DIR + uri;

                for (String resourceSearchDirectory : resourceSearchDirectories) {
                    Resource res = resolveExceptionSafe(resourceSearchDirectory + uriWebAppRelative);
                    if (res.exists()) {
                        resource = res;
                    }
                    else if (!warDeployed) {
                        Resource dir = resolveExceptionSafe(resourceSearchDirectory);
                        if (dir.exists() && info != null) {
                            String filename = dir.getFilename();
                            if (filename != null && filename.equals(info.pluginName)) {
                                Resource pluginFile = dir.createRelative(WEB_APP_DIR + info.uri);
                                if (pluginFile.exists()) {
                                    resource = pluginFile;
                                }
                            }
                        }
                    }
                }
            }

            if (resource == null || !resource.exists()) {
                Resource tmp = defaultResourceLoader != null ? defaultResourceLoader.getResource(uri) : null;
                if (tmp != null && tmp.exists()) {
                    resource = tmp;
                }
            }

            if (resource != null) {
                uriToResourceCache.put(uri, resource);
            }
            else if (warDeployed) {
                uriToResourceCache.put(uri, NULL_RESOURCE);
            }
        }
        return resource == NULL_RESOURCE ? null : resource;
    }

    private PluginResourceInfo inferPluginNameFromURI(String uri) {
        if (uri.startsWith("/plugins/")) {
            String withoutPluginsPath = uri.substring("/plugins/".length(), uri.length());
            int i = withoutPluginsPath.indexOf('/');
            if (i > -1) {
                PluginResourceInfo info = new PluginResourceInfo();
                info.pluginName = withoutPluginsPath.substring(0, i);
                info.uri = withoutPluginsPath.substring(i, withoutPluginsPath.length());
                return info;
            }
        }
        return null;
    }

    public Resource findResourceForClassName(String className) {

        if (className.contains(CLOSURE_MARKER)) {
            className = className.substring(0, className.indexOf(CLOSURE_MARKER));
        }
        Resource resource = classNameToResourceCache.get(className);
        if (resource == null) {
            String classNameWithPathSeparator = className.replace(".", FILE_SEPARATOR);
            for (String pathPattern : getSearchPatternForExtension(classNameWithPathSeparator, ".groovy", ".java")) {
                resource = resolveExceptionSafe(pathPattern);
                if (resource != null && resource.exists()) {
                    classNameToResourceCache.put(className, resource);
                    break;
                }
            }
            if (resource == null || !resource.exists()) {
                for (String ext : new String[]{".groovy", ".java"}) {
                    resource = resolveExceptionSafe(GrailsResourceUtils.DOMAIN_DIR_PATH + "**/" + className + ext);
                    if (resource != null && resource.exists()) {
                        classNameToResourceCache.put(className, resource);
                        break;
                    }
                }
            }
        }
        return resource != null && resource.exists() ? resource : null;
    }

    private List<String> getSearchPatternForExtension(String classNameWithPathSeparator, String... extensions) {

        List<String> searchPatterns = new ArrayList<>();
        for (String extension : extensions) {
            String filename = classNameWithPathSeparator + extension;
            for (String classSearchDirectory : classSearchDirectories) {
                searchPatterns.add(classSearchDirectory + FILE_SEPARATOR + filename);
            }
        }

        return searchPatterns;
    }

    private Resource resolveExceptionSafe(String pathPattern) {
        try {
            Resource[] resources = patchMatchingResolver.getResources("file:" + pathPattern);
            if (resources != null && resources.length > 0) {
                return resources[0];
            }
        } catch (IOException e) {
            return null;
        }
        return null;
    }

    public void setResourceLoader(ResourceLoader resourceLoader) {
        defaultResourceLoader = resourceLoader;
    }

    class PluginResourceInfo {
        String pluginName;
        String uri;
    }
}
