
/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2019 Arno Unkrig. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.codehaus.commons.compiler.jdk.util;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileManager.Location;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardJavaFileManager;

import org.codehaus.commons.compiler.jdk.util.JavaFileObjects.ResourceJavaFileObject;
import org.codehaus.commons.compiler.util.reflect.ApiLog;
import org.codehaus.commons.compiler.util.resource.ListableResourceFinder;
import org.codehaus.commons.compiler.util.resource.Resource;
import org.codehaus.commons.compiler.util.resource.ResourceCreator;
import org.codehaus.commons.compiler.util.resource.ResourceFinder;
import org.codehaus.commons.nullanalysis.NotNullByDefault;

/**
 * Utility methods related to {@link JavaFileManager}s.
 */
public final
class JavaFileManagers {

    private JavaFileManagers() {}

    /**
     * A {@link ForwardingJavaFileManager} that maps accesses to a particular {@link Location} and {@link Kind} to a
     * search in a {@link ResourceFinder}.
     */
    public static <M extends JavaFileManager> ForwardingJavaFileManager<M>
    fromResourceFinder(
        final M              delegate,
        final Location       location,
        final Kind           kind,
        final ResourceFinder resourceFinder,
        final Charset        charset
    ) {

        class ResourceFinderInputJavaFileManager extends ForwardingJavaFileManager<M> {

            ResourceFinderInputJavaFileManager() { super(delegate); }

            @Override @NotNullByDefault(false) public String
            inferBinaryName(Location location, JavaFileObject jfo) {

                if (!(jfo instanceof ResourceJavaFileObject)) {
                    String result = super.inferBinaryName(location, jfo);
                    assert result != null;
                    return result;
                }

                // A [Java]FileObject's "name" looks like this: "/org/codehaus/commons/compiler/Foo.java".
                // A [Java]FileObject's "binary name" looks like "java.lang.annotation.Retention".

                String bn = jfo.getName();
                if (bn.startsWith("/")) bn = bn.substring(1);

                if (!bn.endsWith(jfo.getKind().extension)) {
                    throw new AssertionError(
                        "Name \"" + jfo.getName() + "\" does not match kind \"" + jfo.getKind() + "\""
                    );
                }
                bn = bn.substring(0, bn.length() - jfo.getKind().extension.length());

                bn = bn.replace('/', '.');

                return bn;
            }

            @Override @NotNullByDefault(false) public boolean
            hasLocation(Location location2) { return location2 == location || super.hasLocation(location2); }

            // Must implement "list()", otherwise we'd get "package xyz does not exist" compile errors
            @Override @NotNullByDefault(false) public Iterable<JavaFileObject>
            list(Location location2, String packageName, Set<Kind> kinds, boolean recurse) throws IOException {

                Iterable<JavaFileObject> delegatesJfos = super.list(location2, packageName, kinds, recurse);

                if (location2 == location && kinds.contains(kind)) {

                    assert resourceFinder instanceof ListableResourceFinder : resourceFinder;
                    ListableResourceFinder lrf = (ListableResourceFinder) resourceFinder;

                    Iterable<Resource> resources = lrf.list(packageName.replace('.', '/') + "/", recurse);
                    if (resources != null) {
                        List<JavaFileObject> result = new ArrayList<>();
                        for (Resource r : resources) {

                            String className = r.getFileName();

                            if (!className.endsWith(kind.extension)) continue;
                            className = className.substring(0, className.length() - kind.extension.length());

                            className = className.replace(File.separatorChar, '.');
                            className = className.replace('/',                '.');

                            {
                                final int idx = className.lastIndexOf(packageName + ".");
                                assert idx != -1 : className + "//" + packageName;
                                className = className.substring(idx);
                            }


                            JavaFileObject jfo = this.getJavaFileForInput(location2, className, kind);
                            if (jfo != null) {
                                result.add(jfo);
                            }
                        }

                        // Add JFOs of delegate file manager.
                        for (JavaFileObject jfo : delegatesJfos) result.add(jfo);
                        return result;
                    }
                }

                return delegatesJfos;
            }

            @Override @NotNullByDefault(false) public JavaFileObject
            getJavaFileForInput(Location location2, String className, Kind kind2)
            throws IOException {

                assert location2 != null;
                assert className != null;
                assert kind2     != null;

                if (location2 == location && kind2 == kind) {

                    // Find the source file through the source path.
                    final Resource
                    resource = resourceFinder.findResource(className.replace('.', '/') + kind2.extension);

                    if (resource == null) return null;

                    // Create and return a JavaFileObject.
                    JavaFileObject result = JavaFileObjects.fromResource(resource, className, kind, charset);
                    result = (JavaFileObject) ApiLog.logMethodInvocations(result);
                    return result;
                }

                return super.getJavaFileForInput(location2, className, kind2);
            }

            @Override @NotNullByDefault(false) public boolean
            isSameFile(FileObject a, FileObject b) {

                if (a instanceof ResourceJavaFileObject && b instanceof ResourceJavaFileObject) {
                    return a.getName().contentEquals(b.getName());
                }

                return super.isSameFile(a, b);
            }
        }

        return new ResourceFinderInputJavaFileManager();
    }

    /**
     * @return A {@link ForwardingJavaFileManager} that stores {@link JavaFileObject}s through a {@link ResourceCreator}
     */
    public static <M extends JavaFileManager> ForwardingJavaFileManager<M>
    fromResourceCreator(
        M                     delegate,
        final Location        location,
        final Kind            kind,
        final ResourceCreator resourceCreator,
        final Charset         charset
    ) {

        return new ForwardingJavaFileManager<M>(delegate) {

            @Override @NotNullByDefault(false) public JavaFileObject
            getJavaFileForOutput(
                Location     location2,
                final String className,
                Kind         kind2,
                FileObject   sibling
            ) throws IOException {

                if (kind2 == kind && location2 == location) {
                    final String resourceName = className.replace('.', '/') + ".class";
                    return JavaFileObjects.fromResourceCreator(resourceCreator, resourceName, kind, charset);
                } else {
                    return super.getJavaFileForOutput(location2, className, kind2, sibling);
                }
            }
        };
    }

    /**
     * @return A {@link ForwardingJavaFileManager} that stores {@link JavaFileObject}s in byte arrays, i.e. in memory
     *         (as opposed to the {@link StandardJavaFileManager}, which stores them in files)
     */
    public static <M extends JavaFileManager> ForwardingJavaFileManager<M>
    inMemory(M delegate, final Charset charset) {

        return new ForwardingJavaFileManager<M>(delegate) {

            private final Map<Location, Map<Kind, Map<String /*className*/, JavaFileObject>>>
            javaFiles = new HashMap<>();

            @Override @NotNullByDefault(false) public FileObject
            getFileForInput(Location location, String packageName, String relativeName) {
                throw new UnsupportedOperationException("getFileForInput");
            }

            @Override @NotNullByDefault(false) public FileObject
            getFileForOutput(
                Location   location,
                String     packageName,
                String     relativeName,
                FileObject sibling
            ) {
                throw new UnsupportedOperationException("getFileForOutput");
            }

            @Override @NotNullByDefault(false) public JavaFileObject
            getJavaFileForInput(Location location, String className, Kind kind) throws IOException {

                Map<Kind, Map<String, JavaFileObject>> locationJavaFiles = this.javaFiles.get(location);
                if (locationJavaFiles != null) {
                    Map<String, JavaFileObject> kindJavaFiles = locationJavaFiles.get(kind);
                    if (kindJavaFiles != null) return kindJavaFiles.get(className);
                }

                return super.getJavaFileForInput(location, className, kind);
            }

            @Override @NotNullByDefault(false) public JavaFileObject
            getJavaFileForOutput(
                Location     location,
                final String className,
                Kind         kind,
                FileObject   sibling
            ) throws IOException {

                Map<Kind, Map<String, JavaFileObject>> locationJavaFiles = this.javaFiles.get(location);
                if (locationJavaFiles == null) {
                    locationJavaFiles = new HashMap<>();
                    this.javaFiles.put(location, locationJavaFiles);
                }
                Map<String, JavaFileObject> kindJavaFiles = locationJavaFiles.get(kind);
                if (kindJavaFiles == null) {
                    kindJavaFiles = new HashMap<>();
                    locationJavaFiles.put(kind, kindJavaFiles);
                }

                JavaFileObject fileObject = JavaFileObjects.inMemory(className, kind, charset);

                kindJavaFiles.put(className, fileObject);

                return fileObject;
            }

            @Override @NotNullByDefault(false) public Iterable<JavaFileObject>
            list(
                Location  location,
                String    packageName,
                Set<Kind> kinds,
                boolean   recurse
            ) throws IOException {

                Map<Kind, Map<String, JavaFileObject>> locationFiles = this.javaFiles.get(location);
                if (locationFiles == null) return super.list(location, packageName, kinds, recurse);

                String               prefix = packageName.isEmpty() ? "" : packageName + ".";
                int                  pl     = prefix.length();
                List<JavaFileObject> result = new ArrayList<>();
                for (Kind kind : kinds) {
                    Map<String, JavaFileObject> kindFiles = locationFiles.get(kind);
                    if (kindFiles == null) continue;
                    for (Entry<String, JavaFileObject> e : kindFiles.entrySet()) {
                        final String         className      = e.getKey();
                        final JavaFileObject javaFileObject = e.getValue();

                        if (!className.startsWith(prefix)) continue;
                        if (!recurse && className.indexOf('.', pl) != -1) continue;
                        result.add(javaFileObject);
                    }
                }
                return result;
            }
        };
    }
}
