package org.apache.maven.it;

/*
 * 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.
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.apache.maven.shared.utils.io.IOUtil;

/**
 * Launches an embedded Maven 3.x instance from some Maven installation directory.
 * 
 * @author Benjamin Bentmann
 */
class Embedded3xLauncher
    implements MavenLauncher
{

    private final Object mavenCli;

    private final Method doMain;

    private Embedded3xLauncher( Object mavenCli, Method doMain )
    {
        this.mavenCli = mavenCli;
        this.doMain = doMain;
    }

    /**
     * Launches an embedded Maven 3.x instance from some Maven installation directory.
     */
    public static Embedded3xLauncher createFromMavenHome( String mavenHome, String classworldConf, List<URL> classpath )
        throws LauncherException
    {
        if ( mavenHome == null || mavenHome.length() <= 0 )
        {
            throw new LauncherException( "Invalid Maven home directory " + mavenHome );
        }

        System.setProperty( "maven.home", mavenHome );

        File config;
        if ( classworldConf != null )
        {
            config = new File( classworldConf );
        }
        else
        {
            config = new File( mavenHome, "bin/m2.conf" );
        }

        ClassLoader bootLoader = getBootLoader( mavenHome, classpath );

        ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader( bootLoader );
        try
        {
            Class<?> launcherClass = bootLoader.loadClass( "org.codehaus.plexus.classworlds.launcher.Launcher" );

            Object launcher = launcherClass.newInstance();

            Method configure = launcherClass.getMethod( "configure", new Class[] { InputStream.class } );

            configure.invoke( launcher, new Object[] { new FileInputStream( config ) } );

            Method getWorld = launcherClass.getMethod( "getWorld" );
            Object classWorld = getWorld.invoke( launcher );

            Method getMainClass = launcherClass.getMethod( "getMainClass" );
            Class<?> cliClass = (Class<?>) getMainClass.invoke( launcher );

            Constructor<?> newMavenCli = cliClass.getConstructor( new Class[] { classWorld.getClass() } );
            Object mavenCli = newMavenCli.newInstance( new Object[] { classWorld } );

            Class<?>[] parameterTypes = { String[].class, String.class, PrintStream.class, PrintStream.class };
            Method doMain = cliClass.getMethod( "doMain", parameterTypes );

            return new Embedded3xLauncher( mavenCli, doMain );
        }
        catch ( ReflectiveOperationException | IOException e )
        {
            throw new LauncherException( "Failed to initialize Laucher", e );
        }
        finally
        {
            Thread.currentThread().setContextClassLoader( oldClassLoader );
        }
    }

    /**
     * Launches an embedded Maven 3.x instance from the current class path, i.e. the Maven 3.x dependencies are assumed
     * to be present on the class path.
     */
    public static Embedded3xLauncher createFromClasspath()
        throws LauncherException
    {
        ClassLoader coreLoader = Thread.currentThread().getContextClassLoader();

        try
        {
            Class<?> cliClass = coreLoader.loadClass( "org.apache.maven.cli.MavenCli" );

            Object mavenCli = cliClass.newInstance();

            Class<?>[] parameterTypes = { String[].class, String.class, PrintStream.class, PrintStream.class };
            Method doMain = cliClass.getMethod( "doMain", parameterTypes );

            return new Embedded3xLauncher( mavenCli, doMain );
        }
        catch ( ReflectiveOperationException e )
        {
            throw new LauncherException( e.getMessage(), e );
        }
    }

    private static ClassLoader getBootLoader( String mavenHome, List<URL> classpath )
    {
        List<URL> urls = classpath;

        if ( urls == null )
        {
            urls = new ArrayList<URL>();

            File bootDir = new File( mavenHome, "boot" );
            addUrls( urls, bootDir );
        }

        if ( urls.isEmpty() )
        {
            throw new IllegalArgumentException( "Invalid Maven home directory " + mavenHome );
        }

        URL[] ucp = (URL[]) urls.toArray( new URL[urls.size()] );

        return new URLClassLoader( ucp, ClassLoader.getSystemClassLoader().getParent() );
    }

    private static void addUrls( List<URL> urls, File directory )
    {
        File[] jars = directory.listFiles();

        if ( jars != null )
        {
            for ( int i = 0; i < jars.length; i++ )
            {
                File jar = jars[i];

                if ( jar.getName().endsWith( ".jar" ) )
                {
                    try
                    {
                        urls.add( jar.toURI().toURL() );
                    }
                    catch ( MalformedURLException e )
                    {
                        throw (RuntimeException) new IllegalStateException().initCause( e );
                    }
                }
            }
        }
    }

    public int run( String[] cliArgs, Properties systemProperties, String workingDirectory, File logFile )
        throws IOException, LauncherException
    {
        PrintStream out = ( logFile != null ) ? new PrintStream( new FileOutputStream( logFile ) ) : System.out;
        try
        {
            File workingDirectoryPath = new File( workingDirectory );
            Properties originalProperties = System.getProperties();
            System.setProperties( null );
            System.setProperty( "maven.home", originalProperties.getProperty( "maven.home", "" ) );
            System.setProperty( "user.dir", workingDirectoryPath.getAbsolutePath() );
            System.setProperty( "maven.multiModuleProjectDirectory", 
                    findProjectBaseDirectory( workingDirectoryPath ).getAbsolutePath() );

            for ( Object o : systemProperties.keySet() )
            {
                String key = (String) o;
                String value = systemProperties.getProperty( key );
                System.setProperty( key, value );
            }

            ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader( mavenCli.getClass().getClassLoader() );
            try
            {
                Object result = doMain.invoke( mavenCli, new Object[] { cliArgs, workingDirectory, out, out } );

                return ( (Number) result ).intValue();
            }
            finally
            {
                Thread.currentThread().setContextClassLoader( originalClassLoader );

                System.setProperties( originalProperties );
            }
        }
        catch ( IllegalAccessException | InvocationTargetException e )
        {
            throw new LauncherException( "Failed to run Maven: " + e.getMessage(), e );
        }
        finally
        {
            if ( logFile != null )
            {
                out.close();
            }
        }
    }

    public String getMavenVersion()
        throws LauncherException
    {
        Properties props = new Properties();

        InputStream is =
            mavenCli.getClass().getResourceAsStream( "/META-INF/maven/org.apache.maven/maven-core/pom.properties" );
        if ( is != null )
        {
            try
            {
                props.load( is );
            }
            catch ( IOException e )
            {
                throw new LauncherException( "Failed to read Maven version", e );
            }
            finally
            {
                IOUtil.close( is );
            }
        }

        String version = props.getProperty( "version" );
        if ( version != null )
        {
            return version;
        }

        throw new LauncherException( "Could not determine embedded Maven version" );
    }

    /**
     * Replicates the logic from <a href="https://git.io/JSMug">Maven start script</a> to find 
     * the project's base directory.
     * @param workingDirectory the working directory
     * @return the project's base directory
     */
    private static File findProjectBaseDirectory( File workingDirectory )
    {
        File currentDirectory = workingDirectory;
        // traverses directory structure from process work directory to filesystem root
        // first directory with .mvn subdirectory is considered project base directory
        while ( currentDirectory != null && currentDirectory.getParentFile() != null )
        {
            // see if /.mvn exists
            if ( new File( currentDirectory, ".mvn" ).isDirectory() )
            {
                return currentDirectory;
            }
            currentDirectory = currentDirectory.getParentFile();
        }
        return workingDirectory;
    }
}
