/*
 * Copyright 2011 the original author or authors.
 *
 * Licensed 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.gradle.integtests.fixtures;

import org.gradle.api.JavaVersion;
import org.gradle.api.Nullable;
import org.gradle.api.Transformer;
import org.gradle.api.specs.Spec;
import org.gradle.integtests.fixtures.jvm.InstalledJvmLocator;
import org.gradle.integtests.fixtures.jvm.JvmInstallation;
import org.gradle.internal.SystemProperties;
import org.gradle.internal.jvm.JavaInfo;
import org.gradle.internal.jvm.Jre;
import org.gradle.internal.jvm.Jvm;
import org.gradle.internal.nativeintegration.filesystem.FileCanonicalizer;
import org.gradle.internal.os.OperatingSystem;
import org.gradle.testfixtures.internal.NativeServicesTestFixture;
import org.gradle.util.CollectionUtils;

import java.io.File;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Allows the tests to get hold of an alternative Java installation when needed.
 */
abstract public class AvailableJavaHomes {
    private static List<JvmInstallation> jvms;

    @Nullable
    public static JavaInfo getJava5() {
        return getJdk(JavaVersion.VERSION_1_5);
    }

    /**
     * Locates a JDK installation for the given version.
     *
     * @return null if not found.
     */
    @Nullable
    public static JavaInfo getJdk(JavaVersion version) {
        for (JvmInstallation candidate : getJvms()) {
            if (candidate.getJavaVersion().equals(version) && candidate.isJdk()) {
                return Jvm.forHome(candidate.getJavaHome());
            }
        }
        return null;
    }

    /**
     * Provides all available JDK installations.
     *
     * @return empty list if no JDK can be found.
     */
    public static List<JavaInfo> getAvailableJdks() {
        return CollectionUtils.collect(getJvms(), new Transformer<JavaInfo, JvmInstallation>() {
            public JavaInfo transform(JvmInstallation candidate) {
                return Jvm.forHome(candidate.getJavaHome());
            }
        });
    }

    /**
     * Locates a JDK installation that is different to the current JVM, ie for which java.home is different.
     *
     * @return null if not found.
     */
    @Nullable
    public static JavaInfo getDifferentJdk() {
        Jvm jvm = Jvm.current();
        for (JvmInstallation candidate : getJvms()) {
            if (candidate.getJavaHome().equals(jvm.getJavaHome())) {
                continue;
            }

            // Currently tests implicitly assume a JDK
            if (!candidate.isJdk()) {
                continue;
            }
            return Jvm.forHome(candidate.getJavaHome());
        }

        return null;
    }

    /**
     * Locates a JDK installation that has a different version to the current JVM, ie for which java.version is different.
     *
     * @return null if not found.
     */
    @Nullable
    public static JavaInfo getDifferentVersion() {
        Jvm jvm = Jvm.current();
        for (JvmInstallation candidate : getJvms()) {
            if (candidate.getJavaVersion().equals(jvm.getJavaVersion())) {
                continue;
            }
            // Currently tests implicitly assume a JDK
            if (!candidate.isJdk()) {
                continue;
            }
            return Jvm.forHome(candidate.getJavaHome());
        }

        return null;
    }

    /**
     * Locates a JRE installation for the current JVM. Prefers a stand-alone JRE installation over one that is part of a JDK install.
     *
     * @return The JRE home directory, or null if not found
     */
    public static File getBestJre() {
        Jvm jvm = Jvm.current();
        Jre jre = jvm.getStandaloneJre();
        if (jre != null) {
            return jre.getHomeDir();
        }
        jre = jvm.getJre();
        if (jre != null) {
            return jre.getHomeDir();
        }
        return null;
    }

    private static List<JvmInstallation> getJvms() {
        if (jvms == null) {
            FileCanonicalizer fileCanonicalizer = NativeServicesTestFixture.getInstance().get(FileCanonicalizer.class);
            jvms = new ArrayList<JvmInstallation>();
            jvms.addAll(new DevInfrastructureJvmLocator(fileCanonicalizer).findJvms());
            jvms.addAll(new InstalledJvmLocator().findJvms());
            jvms.addAll(new HomeDirJvmLocator(fileCanonicalizer).findJvms());
            // Order from most recent to least recent
            Collections.sort(jvms, new Comparator<JvmInstallation>() {
                public int compare(JvmInstallation o1, JvmInstallation o2) {
                    return o2.getVersion().compareTo(o1.getVersion());
                }
            });
        }
        System.out.println("Found the following JVMs:");
        for (JvmInstallation jvm : jvms) {
            System.out.println("    " + jvm);
        }
        return jvms;
    }

    private static class DevInfrastructureJvmLocator {
        final FileCanonicalizer fileCanonicalizer;

        private DevInfrastructureJvmLocator(FileCanonicalizer fileCanonicalizer) {
            this.fileCanonicalizer = fileCanonicalizer;
        }

        public List<JvmInstallation> findJvms() {
            List<JvmInstallation> jvms = new ArrayList<JvmInstallation>();
            if (OperatingSystem.current().isLinux()) {
                jvms = addJvm(jvms, JavaVersion.VERSION_1_5, "1.5.0", new File("/opt/jdk/sun-jdk-5"), true, JvmInstallation.Arch.i386);
                jvms = addJvm(jvms, JavaVersion.VERSION_1_6, "1.6.0", new File("/opt/jdk/sun-jdk-6"), true, JvmInstallation.Arch.x86_64);
                jvms = addJvm(jvms, JavaVersion.VERSION_1_6, "1.6.0", new File("/opt/jdk/ibm-jdk-6"), true, JvmInstallation.Arch.x86_64);
                jvms = addJvm(jvms, JavaVersion.VERSION_1_7, "1.7.0", new File("/opt/jdk/oracle-jdk-7"), true, JvmInstallation.Arch.x86_64);
                jvms = addJvm(jvms, JavaVersion.VERSION_1_8, "1.8.0", new File("/opt/jdk/oracle-jdk-8"), true, JvmInstallation.Arch.x86_64);
            }
            return CollectionUtils.filter(jvms, new Spec<JvmInstallation>() {
                public boolean isSatisfiedBy(JvmInstallation element) {
                    return element.getJavaHome().isDirectory();
                }
            });
        }

        private List<JvmInstallation> addJvm(List<JvmInstallation> jvms, JavaVersion javaVersion, String versionString, File javaHome, boolean jdk, JvmInstallation.Arch arch) {
            if(javaHome.exists()) {
                jvms.add(new JvmInstallation(javaVersion, versionString, fileCanonicalizer.canonicalize(javaHome), jdk, arch));
            }
            return jvms;
        }
    }

    private static class HomeDirJvmLocator {
        private static final Pattern JDK_DIR = Pattern.compile("jdk(\\d+\\.\\d+\\.\\d+(_\\d+)?)");
        final FileCanonicalizer fileCanonicalizer;

        private HomeDirJvmLocator(FileCanonicalizer fileCanonicalizer) {
            this.fileCanonicalizer = fileCanonicalizer;
        }

        public List<JvmInstallation> findJvms() {
            Set<File> javaHomes = new HashSet<File>();
            List<JvmInstallation> jvms = new ArrayList<JvmInstallation>();
            for (File file : new File(SystemProperties.getInstance().getUserHome()).listFiles()) {
                Matcher matcher = JDK_DIR.matcher(file.getName());
                if (!matcher.matches()) {
                    continue;
                }
                File javaHome = fileCanonicalizer.canonicalize(file);
                if (!javaHomes.add(javaHome)) {
                    continue;
                }
                if (!new File(file, "bin/javac").isFile()) {
                    continue;
                }
                String version = matcher.group(1);
                jvms.add(new JvmInstallation(JavaVersion.toVersion(version), version, file, true, JvmInstallation.Arch.Unknown));
            }
            return jvms;
        }
    }
}
