/*
 * 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.netbeans.modules.java.source;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.spi.java.classpath.PathResourceImplementation;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.util.BaseUtilities;

import static junit.framework.TestCase.assertFalse;
import org.openide.filesystems.MultiFileSystem;
import org.openide.filesystems.Repository;

/** Note this is duplicated in:
 *  java.hints.test/src/org/netbeans/modules/java/hints/test/BootClassPathUtil.java
 *
 * @author lahvac
 */
public class BootClassPathUtil {

    public static ClassPath getBootClassPath() {
        String cp = System.getProperty("sun.boot.class.path");
        if (cp != null) {
            List<URL> urls = new ArrayList<>();
            String[] paths = cp.split(Pattern.quote(System.getProperty("path.separator")));
            for (String path : paths) {
                File f = new File(path);

                if (!f.canRead())
                    continue;

                FileObject fo = FileUtil.toFileObject(f);
                if (FileUtil.isArchiveFile(fo)) {
                    fo = FileUtil.getArchiveRoot(fo);
                }
                if (fo != null) {
                    urls.add(fo.toURL());
                }
            }
            return ClassPathSupport.createClassPath((URL[])urls.toArray(new URL[0]));
        } else {
            try {
                Class.forName("org.netbeans.ProxyURLStreamHandlerFactory").getMethod("register")
                                                                          .invoke(null);
            } catch (ClassNotFoundException | NoSuchMethodException |
                     SecurityException | IllegalAccessException |
                    IllegalArgumentException | InvocationTargetException ex) {
                throw new IllegalStateException(ex);
            }
            final List<PathResourceImplementation> modules = new ArrayList<>();
            final File installDir = new File(System.getProperty("java.home"));
            final URI imageURI = getImageURI(installDir);
            try {
                final FileObject jrtRoot = URLMapper.findFileObject(imageURI.toURL());
                final FileObject root = getModulesRoot(jrtRoot);
                for (FileObject module : root.getChildren()) {
                    modules.add(ClassPathSupport.createResource(module.toURL()));
                }
            } catch (MalformedURLException e) {
                throw new IllegalStateException(e);
            }
            assertFalse(modules.isEmpty());
            return ClassPathSupport.createClassPath(modules);
        }
    }

    public static ClassPath getModuleBootPath() {
        if (System.getProperty("sun.boot.class.path") != null) {
            try {
                //JDK 8:
                return getModuleBootOnJDK8();
            } catch (Exception ex) {
                throw new IllegalStateException(ex);
            }
        } else {
            return getBootClassPath();
        }
    }

    private static final String PROTOCOL = "nbjrt"; //NOI18N

    private static URI getImageURI(@NonNull final File jdkHome) {
        try {
            return new URI(String.format(
                "%s:%s!/%s",  //NOI18N
                PROTOCOL,
                BaseUtilities.toURI(jdkHome).toString(),
                ""));
        } catch (URISyntaxException e) {
            throw new IllegalStateException();
        }
    }

    @NonNull
    private static FileObject getModulesRoot(@NonNull final FileObject jrtRoot) {
        final FileObject modules = jrtRoot.getFileObject("modules");    //NOI18N
        //jimage v1 - modules are located in the root
        //jimage v2 - modules are located in "modules" folder
        return modules == null ?
            jrtRoot :
            modules;
    }

    //create fake "system module path" while running on JDK 8
    //java.base contains rt.jar and exports all java.* packages:
    private static ClassPath moduleBootOnJDK8;

    private static ClassPath getModuleBootOnJDK8() throws Exception {
        if (moduleBootOnJDK8 == null) {
            List<FileSystem> roots = new ArrayList<>();

            FileSystem output = FileUtil.createMemoryFileSystem();
            FileObject sink = output.getRoot();

            roots.add(output);

            Set<String> packages = new HashSet<>();
            for (FileObject r : getBootClassPath().getRoots()) {
                FileObject javaDir = r.getFileObject("java");

                if (javaDir == null)
                    continue;

                roots.add(r.getFileSystem());

                Enumeration<? extends FileObject> c = javaDir.getChildren(true);

                while (c.hasMoreElements()) {
                    FileObject current = c.nextElement();

                    if (!current.isData() || !current.hasExt("class")) continue;

                    String rel = FileUtil.getRelativePath(r, current.getParent());

                    packages.add(rel.replace('/', '.'));
                }
            }

            FileSystem outS = new MultiFileSystem(roots.toArray(new FileSystem[0])) {
                {
                    setSystemName("module-boot");
                }
            };

            Repository.getDefault().addFileSystem(outS);

            StringBuilder moduleInfo = new StringBuilder();

            moduleInfo.append("module java.base {\n");

            for (String pack : packages) {
                moduleInfo.append("    exports " + pack + ";\n");
            }

            moduleInfo.append("}\n");

            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            FileObject javaBase = outS.getRoot();

            try (JavaFileManager fm = compiler.getStandardFileManager(null, null, null);
                 JFMImpl fmImpl = new JFMImpl(fm, javaBase, sink)) {
                compiler.getTask(null, fmImpl, null, Arrays.asList("-proc:none"), null,
                                 Arrays.asList(new SimpleJavaFileObject(new URI("mem:///module-info.java"), javax.tools.JavaFileObject.Kind.SOURCE) {
                    @Override
                    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
                        return moduleInfo.toString();
                    }
                })).call();
            }

            javaBase.refresh();
            moduleBootOnJDK8 = ClassPathSupport.createClassPath(javaBase);
        }

        return moduleBootOnJDK8;
    }

    private static class JFMImpl extends ForwardingJavaFileManager<JavaFileManager> {
        private final FileObject output;
        private final FileObject sink;

        public JFMImpl(JavaFileManager fileManager, FileObject output, FileObject sink) {
            super(fileManager);
            this.output = output;
            this.sink = sink;
        }

        @Override
        public Iterable<Set<Location>> listLocationsForModules(Location location) throws IOException {
            return Collections.emptyList();
        }

        @Override
        public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
            if (location == StandardLocation.CLASS_OUTPUT) {
                List<JavaFileObject> result = new ArrayList<>();
                FileObject pack = output.getFileObject(packageName.replace('.', '/'));

                if (pack != null) {
                    Enumeration<? extends FileObject> c = pack.getChildren(recurse);

                    while (c.hasMoreElements()) {
                        FileObject file = c.nextElement();
                        if (!file.hasExt("class"))
                            continue;
                        result.add(new InferableJavaFileObject(file, JavaFileObject.Kind.CLASS));
                    }
                }

                return result;
            }
            return super.list(location, packageName, kinds, recurse);
        }

        @Override
        public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, javax.tools.FileObject sibling) throws IOException {
            if (location == StandardLocation.CLASS_OUTPUT) {
                String relPath = className.replace('.', '/') + ".class";
                try {
                    return new SimpleJavaFileObject(new URI("mem://" + relPath), kind) {
                        @Override
                        public OutputStream openOutputStream() throws IOException {
                            return new ByteArrayOutputStream() {
                                @Override
                                public void close() throws IOException {
                                    super.close();
                                    FileObject target = FileUtil.createData(sink, relPath);
                                    try (OutputStream out = target.getOutputStream()) {
                                        out.write(toByteArray());
                                    }
                                }
                            };
                        }
                    };
                } catch (URISyntaxException ex) {
                    throw new IOException(ex);
                }
            }
            return super.getJavaFileForOutput(location, className, kind, sibling);
        }

        @Override
        public String inferBinaryName(Location location, JavaFileObject file) {
            if (file instanceof InferableJavaFileObject) {
                return ((InferableJavaFileObject) file).className;
            }
            return super.inferBinaryName(location, file);
        }

        private class InferableJavaFileObject extends SimpleJavaFileObject {

            private final String className;
            public InferableJavaFileObject(FileObject file, Kind kind) {
                super(file.toURI(), kind);
                String relPath = FileUtil.getRelativePath(output, file);
                className = relPath.substring(0, relPath.length() - ".class".length()).replace('/', '.');
            }
        }

    }

}
