/*
 * Copyright 2009 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.openapi.external;

import org.gradle.foundation.BootstrapLoader;

import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.regex.Pattern;

/**

 Utility functions required by the OpenAPI

 @author mhunsicker
  */
public class ExternalUtility
{
    private static final Pattern GRADLE_CORE_PATTERN = Pattern.compile("^gradle-core-\\d.*\\.jar$");

   /*
      Call this to get a classloader that has loaded gradle.

      @param  parentClassLoader    Your classloader. Probably the classloader
                                   of whatever class is calling this.
      @param  gradleHomeDirectory  the root directory of a gradle installation
      @param  showDebugInfo        true to show some additional information that
                                   may be helpful diagnosing problems is this
                                   fails
      @return a classloader that has loaded gradle and all of its dependencies.
      @author mhunsicker
   */

    public static ClassLoader getGradleClassloader(ClassLoader parentClassLoader, File gradleHomeDirectory,
                                                   boolean showDebugInfo) throws Exception {
        File gradleJarFile = getGradleJar(gradleHomeDirectory);
        if (gradleJarFile == null) {
            throw new RuntimeException(
                    "Not a valid gradle home directory '" + gradleHomeDirectory.getAbsolutePath() + "'");
        }

        System.setProperty("gradle.home", gradleHomeDirectory.getAbsolutePath());

        BootstrapLoader bootstrapLoader = new BootstrapLoader();
        bootstrapLoader.initialize(parentClassLoader, gradleHomeDirectory, true, false, showDebugInfo);
        return bootstrapLoader.getClassLoader();
    }

    /*
       This locates the gradle jar. We do NOT want the gradle-wrapper jar.

       @param  gradleHomeDirectory the root directory of a gradle installation.
                                   We're expecting this to have a child directory
                                   named 'lib'.
       @return the gradle jar file. Null if we didn't find it.
       @author mhunsicker
    */

    public static File getGradleJar(File gradleHomeDirectory) {
        File libDirectory = new File(gradleHomeDirectory, "lib");
        if (!libDirectory.exists()) {
            return null;
        }

        //try to get the gradle.jar. It'll be "gradle-[version].jar"
        File[] files = libDirectory.listFiles(new FileFilter() {
            public boolean accept(File file) {
                return GRADLE_CORE_PATTERN.matcher(file.getName()).matches();
            }
        });

        if (files == null || files.length == 0) {
            return null;
        }

        //if they've given us a directory with multiple gradle jars, tell them. We won't know which one to use.
        if (files.length > 1) {
            throw new RuntimeException(
                    "Installation has multiple gradle jars. Cannot determine which one to use. Found files: " + createFileNamesString(files));
        }

        return files[0];
    }

    private static StringBuilder createFileNamesString(File[] files) {
        StringBuilder fileNames = new StringBuilder();
        for (File f : files)
        {
            fileNames.append(f.getName() + ", ");
        }
        fileNames.delete(fileNames.length() - 2, fileNames.length()); // Remove the trailing ', '
        return fileNames;
    }

    //just a function to help debugging. If we can't find the constructor we want, this dumps out what is available.

    public static String dumpConstructors(Class classInQuestion) {
        StringBuilder builder = new StringBuilder();
        Constructor[] constructors = classInQuestion.getConstructors();
        for (int index = 0; index < constructors.length; index++) {
            Constructor constructor = constructors[index];
            builder.append(constructor).append('\n');
        }

        return builder.toString();
    }


    public static String dumpMethods(Class classInQuestion) {
        StringBuilder builder = new StringBuilder();

        Method[] methods = classInQuestion.getMethods();
        for (int index = 0; index < methods.length; index++) {
            Method method = methods[index];
            builder.append(method).append('\n');
        }

        return builder.toString();
    }

    /**
     * This attempts to load the a class from the specified gradle home directory.
     * @param classToLoad  the full path to the class to load
     * @param  parentClassLoader    Your classloader. Probably the classloader
     *                              of whatever class is calling this.
     * @param  gradleHomeDirectory  the root directory of a gradle installation
     * @param  showDebugInfo        true to show some additional information that
     *                              may be helpful diagnosing problems is this
     *                              fails
     */
    public static Class loadGradleClass( String classToLoad, ClassLoader parentClassLoader, File gradleHomeDirectory, boolean showDebugInfo ) throws Exception
    {
       ClassLoader bootStrapClassLoader = getGradleClassloader( parentClassLoader, gradleHomeDirectory, showDebugInfo );
       Thread.currentThread().setContextClassLoader(bootStrapClassLoader);

       //load the class in gradle that wraps our return interface and handles versioning issues.
      try
      {
         return bootStrapClassLoader.loadClass( classToLoad );
      }
      catch( NoClassDefFoundError e )
      {  //might be a version mismatch
         e.printStackTrace();
         return null;
      }
      catch( Throwable e )
      {  //might be a version mismatch
         e.printStackTrace();
         return null;
      }
    }

     /**
     * This wraps up invoking a static method into a single call.
     * @param classToInvoke the class that has the method
     * @param methodName    the name of the method to invoke
     * @param argumentsClasses the classes of the arguments (we can't determine this from the argumentValues
     *                          because they can be of class A, but implement class B and B is be the argument
     *                          type of the method in question
     * @param argumentValues   the values of the arguments.
     * @return the return value of invoking the method.
     * @throws Exception
     */
    public static Object invokeStaticMethod( Class classToInvoke, String methodName, Class[] argumentsClasses, Object ... argumentValues) throws Exception
    {
      Method method = null;
      try
      {
         method = classToInvoke.getDeclaredMethod( methodName, argumentsClasses );
      }
      catch( NoSuchMethodException e )
      {
         e.printStackTrace();
         System.out.println( "Dumping available methods on " + classToInvoke.getName() + "\n" + ExternalUtility.dumpMethods( classToInvoke ) );
         throw e;
      }
      return method.invoke( null, argumentValues);
    }
}
