package org.apache.maven.plugin.surefire.booterclient;

/*
 * 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 org.apache.maven.plugin.surefire.booterclient.lazytestprovider.OutputStreamFlushableCommandline;
import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
import org.apache.maven.surefire.booter.AbstractPathConfiguration;
import org.apache.maven.surefire.booter.Classpath;
import org.apache.maven.surefire.booter.ModularClasspath;
import org.apache.maven.surefire.booter.ModularClasspathConfiguration;
import org.apache.maven.surefire.booter.StartupConfiguration;
import org.apache.maven.surefire.booter.SurefireBooterForkException;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ModuleVisitor;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import static java.io.File.createTempFile;
import static java.io.File.pathSeparatorChar;
import static org.apache.maven.plugin.surefire.SurefireHelper.escapeToPlatformPath;
import static org.apache.maven.shared.utils.StringUtils.replace;
import static org.apache.maven.surefire.util.internal.StringUtils.NL;
import static org.objectweb.asm.Opcodes.ASM7;

/**
 * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
 * @since 2.21.0.Jigsaw
 */
public class ModularClasspathForkConfiguration
        extends DefaultForkConfiguration
{
    @SuppressWarnings( "checkstyle:parameternumber" )
    public ModularClasspathForkConfiguration( @Nonnull Classpath bootClasspath,
                                              @Nonnull File tempDirectory,
                                              @Nullable String debugLine,
                                              @Nonnull File workingDirectory,
                                              @Nonnull Properties modelProperties,
                                              @Nullable String argLine,
                                              @Nonnull Map<String, String> environmentVariables,
                                              boolean debug,
                                              @Nonnegative int forkCount,
                                              boolean reuseForks,
                                              @Nonnull Platform pluginPlatform,
                                              @Nonnull ConsoleLogger log )
    {
        super( bootClasspath, tempDirectory, debugLine, workingDirectory, modelProperties, argLine,
                environmentVariables, debug, forkCount, reuseForks, pluginPlatform, log );
    }

    @Override
    protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli, @Nonnull String startClass,
                                     @Nonnull StartupConfiguration config, @Nonnull File dumpLogDirectory )
            throws SurefireBooterForkException
    {
        try
        {
            AbstractPathConfiguration pathConfig = config.getClasspathConfiguration();

            ModularClasspathConfiguration modularClasspathConfiguration =
                    pathConfig.toRealPath( ModularClasspathConfiguration.class );

            ModularClasspath modularClasspath = modularClasspathConfiguration.getModularClasspath();

            File descriptor = modularClasspath.getModuleDescriptor();
            List<String> modulePath = modularClasspath.getModulePath();
            Collection<String> packages = modularClasspath.getPackages();
            File patchFile = modularClasspath.getPatchFile();
            List<String> classpath = toCompleteClasspath( config );

            File argsFile = createArgsFile( descriptor, modulePath, classpath, packages, patchFile, startClass );

            cli.createArg().setValue( "@" + escapeToPlatformPath( argsFile.getAbsolutePath() ) );
        }
        catch ( IOException e )
        {
            String error = "Error creating args file";
            InPluginProcessDumpSingleton.getSingleton()
                    .dumpException( e, error, dumpLogDirectory );
            throw new SurefireBooterForkException( error, e );
        }
    }

    @Nonnull
    File createArgsFile( @Nonnull File moduleDescriptor, @Nonnull List<String> modulePath,
                         @Nonnull List<String> classPath, @Nonnull Collection<String> packages,
                         @Nonnull File patchFile, @Nonnull String startClassName )
            throws IOException
    {
        File surefireArgs = createTempFile( "surefireargs", "", getTempDirectory() );
        if ( isDebug() )
        {
            getLogger().debug( "Path to args file: " +  surefireArgs.getCanonicalPath() );
        }
        else
        {
            surefireArgs.deleteOnExit();
        }

        try ( FileWriter io = new FileWriter( surefireArgs ) )
        {
            StringBuilder args = new StringBuilder( 64 * 1024 );
            if ( !modulePath.isEmpty() )
            {
                // https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-4856361B-8BFD-4964-AE84-121F5F6CF111
                args.append( "--module-path" )
                        .append( NL )
                        .append( '"' );

                for ( Iterator<String> it = modulePath.iterator(); it.hasNext(); )
                {
                    args.append( replace( it.next(), "\\", "\\\\" ) );
                    if ( it.hasNext() )
                    {
                        args.append( pathSeparatorChar );
                    }
                }

                args.append( '"' )
                        .append( NL );
            }

            if ( !classPath.isEmpty() )
            {
                args.append( "--class-path" )
                        .append( NL )
                        .append( '"' );

                for ( Iterator<String> it = classPath.iterator(); it.hasNext(); )
                {
                    args.append( replace( it.next(), "\\", "\\\\" ) );
                    if ( it.hasNext() )
                    {
                        args.append( pathSeparatorChar );
                    }
                }

                args.append( '"' )
                        .append( NL );
            }

            final String moduleName = toModuleName( moduleDescriptor );

            args.append( "--patch-module" )
                    .append( NL )
                    .append( moduleName )
                    .append( '=' )
                    .append( '"' )
                    .append( replace( patchFile.getPath(), "\\", "\\\\" ) )
                    .append( '"' )
                    .append( NL );

            for ( String pkg : packages )
            {
                args.append( "--add-exports" )
                        .append( NL )
                        .append( moduleName )
                        .append( '/' )
                        .append( pkg )
                        .append( '=' )
                        .append( "ALL-UNNAMED" )
                        .append( NL );
            }

            args.append( "--add-modules" )
                    .append( NL )
                    .append( moduleName )
                    .append( NL );

            args.append( "--add-reads" )
                    .append( NL )
                    .append( moduleName )
                    .append( '=' )
                    .append( "ALL-UNNAMED" )
                    .append( NL );

            args.append( startClassName );

            String argsFileContent = args.toString();

            if ( isDebug() )
            {
                getLogger().debug( "args file content:" + NL + argsFileContent );
            }

            io.write( argsFileContent );

            return surefireArgs;
        }
    }

    @Nonnull
    String toModuleName( @Nonnull File moduleDescriptor ) throws IOException
    {
        if ( !moduleDescriptor.isFile() )
        {
            throw new IOException( "No such Jigsaw module-descriptor exists " + moduleDescriptor.getAbsolutePath() );
        }

        final StringBuilder sb = new StringBuilder();
        new ClassReader( new FileInputStream( moduleDescriptor ) ).accept( new ClassVisitor( ASM7 )
        {
            @Override
            public ModuleVisitor visitModule( String name, int access, String version )
            {
                sb.setLength( 0 );
                sb.append( name );
                return super.visitModule( name, access, version );
            }
        }, 0 );

        return sb.toString();
    }
}
