package org.apache.maven.plugins.assembly.archive;

/*
 * 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 javax.inject.Inject;
import javax.inject.Named;

import org.apache.maven.plugins.assembly.AssemblerConfigurationSource;
import org.apache.maven.plugins.assembly.InvalidAssemblerConfigurationException;
import org.apache.maven.plugins.assembly.archive.archiver.AssemblyProxyArchiver;
import org.apache.maven.plugins.assembly.archive.phase.AssemblyArchiverPhase;
import org.apache.maven.plugins.assembly.archive.phase.AssemblyArchiverPhaseComparator;
import org.apache.maven.plugins.assembly.artifact.DependencyResolutionException;
import org.apache.maven.plugins.assembly.filter.ComponentsXmlArchiverFileFilter;
import org.apache.maven.plugins.assembly.filter.ContainerDescriptorHandler;
import org.apache.maven.plugins.assembly.format.AssemblyFormattingException;
import org.apache.maven.plugins.assembly.internal.DebugConfigurationListener;
import org.apache.maven.plugins.assembly.interpolation.AssemblyExpressionEvaluator;
import org.apache.maven.plugins.assembly.model.Assembly;
import org.apache.maven.plugins.assembly.model.ContainerDescriptorHandlerConfig;
import org.apache.maven.plugins.assembly.utils.AssemblyFileUtils;
import org.apache.maven.plugins.assembly.utils.AssemblyFormatUtils;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.archiver.ArchiveFinalizer;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.diags.DryRunArchiver;
import org.codehaus.plexus.archiver.filters.JarSecurityFileSelector;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.codehaus.plexus.archiver.tar.TarArchiver;
import org.codehaus.plexus.archiver.tar.TarLongFileMode;
import org.codehaus.plexus.archiver.war.WarArchiver;
import org.codehaus.plexus.archiver.zip.AbstractZipArchiver;
import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
import org.codehaus.plexus.component.configurator.ComponentConfigurator;
import org.codehaus.plexus.component.configurator.ConfigurationListener;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.components.io.fileselectors.FileSelector;
import org.codehaus.plexus.configuration.PlexusConfiguration;
import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;

import static java.util.Objects.requireNonNull;

/**
 * Controller component designed to organize the many activities involved in creating an assembly archive. This includes
 * locating and configuring {@link Archiver} instances, executing multiple {@link org.apache.maven.plugins.assembly
 * .archive.phase.AssemblyArchiverPhase} instances to
 * interpret the various sections of the assembly descriptor and determine which files to add, and other associated
 * activities.
 *
 *
 */
@Named
public class DefaultAssemblyArchiver implements AssemblyArchiver
{
    private static final Logger LOGGER = LoggerFactory.getLogger( DefaultAssemblyArchiver.class );

    private final ArchiverManager archiverManager;

    private final List<AssemblyArchiverPhase> assemblyPhases;

    @SuppressWarnings( "MismatchedQueryAndUpdateOfCollection" )
    private final Map<String, ContainerDescriptorHandler> containerDescriptorHandlers;

    private final PlexusContainer container;


    @Inject
    public DefaultAssemblyArchiver( ArchiverManager archiverManager, List<AssemblyArchiverPhase> assemblyPhases,
                                    Map<String, ContainerDescriptorHandler> containerDescriptorHandlers,
                                    PlexusContainer container )
    {
        this.archiverManager = requireNonNull( archiverManager );
        this.assemblyPhases = requireNonNull( assemblyPhases );
        this.containerDescriptorHandlers = requireNonNull( containerDescriptorHandlers );
        this.container = requireNonNull( container );
    }

    private List<AssemblyArchiverPhase> sortedPhases()
    {
        List<AssemblyArchiverPhase> sorted = new ArrayList<>( assemblyPhases );
        Collections.sort( sorted, new AssemblyArchiverPhaseComparator() );
        return sorted;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public File createArchive( final Assembly assembly, final String fullName, final String format,
                               final AssemblerConfigurationSource configSource, boolean recompressZippedFiles,
                               String mergeManifestMode, Date outputTimestamp )
        throws ArchiveCreationException, AssemblyFormattingException, InvalidAssemblerConfigurationException
    {
        validate( assembly );

        String filename = fullName;
        if ( !configSource.isIgnoreDirFormatExtensions() || !format.startsWith( "dir" ) )
        {
            filename += "." + format;
        }

        AssemblyFileUtils.verifyTempDirectoryAvailability( configSource.getTemporaryRootDirectory() );

        final File outputDirectory = configSource.getOutputDirectory();

        final File destFile = new File( outputDirectory, filename );

        try
        {
            final String finalName = configSource.getFinalName();
            final String specifiedBasedir = assembly.getBaseDirectory();

            String basedir = finalName;

            if ( specifiedBasedir != null )
            {
                basedir = AssemblyFormatUtils.getOutputDirectory( specifiedBasedir, finalName, configSource,
                                                                  AssemblyFormatUtils.moduleProjectInterpolator(
                                                                      configSource.getProject() ),
                                                                  AssemblyFormatUtils.artifactProjectInterpolator(
                                                                      null ) );
            }

            final List<ContainerDescriptorHandler> containerHandlers =
                selectContainerDescriptorHandlers( assembly.getContainerDescriptorHandlers(), configSource );

            final Archiver archiver =
                createArchiver( format, assembly.isIncludeBaseDirectory(), basedir, configSource, containerHandlers,
                                recompressZippedFiles, mergeManifestMode, outputTimestamp );

            archiver.setDestFile( destFile );

            for ( AssemblyArchiverPhase phase : sortedPhases() )
            {
                phase.execute( assembly, archiver, configSource );
            }

            archiver.createArchive();
        }
        catch ( final ArchiverException | IOException e )
        {
            throw new ArchiveCreationException(
                "Error creating assembly archive " + assembly.getId() + ": " + e.getMessage(), e );
        }
        catch ( final NoSuchArchiverException e )
        {
            throw new ArchiveCreationException(
                "Unable to obtain archiver for extension '" + format + "', for assembly: '" + assembly.getId() + "'",
                e );
        }
        catch ( final DependencyResolutionException e )
        {
            throw new ArchiveCreationException(
                "Unable to resolve dependencies for assembly '" + assembly.getId() + "'", e );
        }

        return destFile;
    }

    private void validate( final Assembly assembly )
        throws InvalidAssemblerConfigurationException
    {
        if ( assembly.getId() == null || assembly.getId().trim().length() < 1 )
        {
            throw new InvalidAssemblerConfigurationException( "Assembly ID must be present and non-empty." );
        }
    }

    // CHECKSTYLE_OFF: LineLength
    private List<ContainerDescriptorHandler> selectContainerDescriptorHandlers(
        List<ContainerDescriptorHandlerConfig> requestedContainerDescriptorHandlers,
        final AssemblerConfigurationSource configSource )
        throws InvalidAssemblerConfigurationException
    // CHECKSTYLE_ON: LineLength
    {
        LOGGER.debug( "All known ContainerDescriptorHandler components: " + ( containerDescriptorHandlers == null
            ? "none; map is null."
            : "" + containerDescriptorHandlers.keySet() ) );

        if ( requestedContainerDescriptorHandlers == null )
        {
            requestedContainerDescriptorHandlers = new ArrayList<>();
        }

        final List<ContainerDescriptorHandler> handlers = new ArrayList<>();
        final List<String> hints = new ArrayList<>();

        if ( !requestedContainerDescriptorHandlers.isEmpty() )
        {
            for ( final ContainerDescriptorHandlerConfig config : requestedContainerDescriptorHandlers )
            {
                final String hint = config.getHandlerName();
                final ContainerDescriptorHandler handler = containerDescriptorHandlers.get( hint );

                if ( handler == null )
                {
                    throw new InvalidAssemblerConfigurationException(
                        "Cannot find ContainerDescriptorHandler with hint: " + hint );
                }

                LOGGER.debug(
                    "Found container descriptor handler with hint: " + hint + " (component: " + handler + ")" );

                if ( config.getConfiguration() != null )
                {
                    LOGGER.debug( "Configuring handler with:\n\n" + config.getConfiguration() + "\n\n" );

                    configureContainerDescriptorHandler( handler, (Xpp3Dom) config.getConfiguration(), configSource );
                }

                handlers.add( handler );
                hints.add( hint );
            }
        }

        if ( !hints.contains( "plexus" ) )
        {
            handlers.add( new ComponentsXmlArchiverFileFilter() );
        }

        return handlers;
    }

    /**
     * Creates the necessary archiver to build the distribution file.
     *
     * @param format                Archive format
     * @param includeBaseDir        the base directory for include.
     * @param finalName             The final name.
     * @param configSource          {@link AssemblerConfigurationSource}
     * @param containerHandlers     The list of {@link ContainerDescriptorHandler}
     * @param recompressZippedFiles recompress zipped files.
     * @param mergeManifestMode     how to handle already existing Manifest files
     * @return archiver Archiver generated
     * @throws org.codehaus.plexus.archiver.ArchiverException
     * @throws org.codehaus.plexus.archiver.manager.NoSuchArchiverException
     */
    protected Archiver createArchiver( final String format, final boolean includeBaseDir, final String finalName,
                                       final AssemblerConfigurationSource configSource,
                                       final List<ContainerDescriptorHandler> containerHandlers,
                                       boolean recompressZippedFiles, String mergeManifestMode, Date outputTimestamp )
        throws NoSuchArchiverException
    {
        Archiver archiver;
        if ( "txz".equals( format ) || "tgz".equals( format ) || "tbz2".equals( format ) || format.startsWith( "tar" ) )
        {
            archiver = createTarArchiver( format, TarLongFileMode.valueOf( configSource.getTarLongFileMode() ) );
        }
        else if ( "war".equals( format ) )
        {
            archiver = createWarArchiver();
        }
        else
        {
            archiver = archiverManager.getArchiver( format );
        }

        if ( archiver instanceof AbstractZipArchiver )
        {
            ( (AbstractZipArchiver) archiver ).setRecompressAddedZips( recompressZippedFiles );
        }

        final List<FileSelector> extraSelectors = new ArrayList<>();
        final List<ArchiveFinalizer> extraFinalizers = new ArrayList<>();
        if ( archiver instanceof JarArchiver )
        {
            if ( mergeManifestMode != null )
            {
                ( (JarArchiver) archiver ).setFilesetmanifest(
                    JarArchiver.FilesetManifestConfig.valueOf( mergeManifestMode ) );
            }

            extraSelectors.add( new JarSecurityFileSelector() );

            extraFinalizers.add(
                new ManifestCreationFinalizer( configSource.getMavenSession(), configSource.getProject(),
                                               configSource.getJarArchiveConfiguration() ) );

        }

        if ( configSource.getArchiverConfig() != null )
        {
            configureArchiver( archiver, configSource );
        }

        String prefix = "";
        if ( includeBaseDir )
        {
            prefix = finalName;
        }

        archiver = new AssemblyProxyArchiver( prefix, archiver, containerHandlers, extraSelectors, extraFinalizers,
                                              configSource.getWorkingDirectory() );
        if ( configSource.isDryRun() )
        {
            archiver = new DryRunArchiver( archiver, LOGGER );
        }

        archiver.setUseJvmChmod( configSource.isUpdateOnly() );
        archiver.setIgnorePermissions( configSource.isIgnorePermissions() );
        archiver.setForced( !configSource.isUpdateOnly() );

        // configure for Reproducible Builds based on outputTimestamp value
        if ( outputTimestamp != null )
        {
            archiver.configureReproducible( outputTimestamp );
        }

        if ( configSource.getOverrideUid() != null )
        {
            archiver.setOverrideUid( configSource.getOverrideUid() );
        }
        if ( StringUtils.isNotBlank( configSource.getOverrideUserName() ) )
        {
            archiver.setOverrideUserName( StringUtils.trim( configSource.getOverrideUserName() ) );
        }
        if ( configSource.getOverrideGid() != null )
        {
            archiver.setOverrideGid( configSource.getOverrideGid() );
        }
        if ( StringUtils.isNotBlank( configSource.getOverrideGroupName() ) )
        {
            archiver.setOverrideGroupName( StringUtils.trim( configSource.getOverrideGroupName() ) );
        }

        return archiver;
    }

    private void configureContainerDescriptorHandler( final ContainerDescriptorHandler handler, final Xpp3Dom config,
                                                      final AssemblerConfigurationSource configSource )
        throws InvalidAssemblerConfigurationException
    {
        LOGGER.debug( "Configuring handler: '" + handler.getClass().getName() + "' -->" );

        try
        {
            configureComponent( handler, config, configSource );
        }
        catch ( final ComponentConfigurationException e )
        {
            throw new InvalidAssemblerConfigurationException(
                "Failed to configure handler: " + handler.getClass().getName(), e );
        }
        catch ( final ComponentLookupException e )
        {
            throw new InvalidAssemblerConfigurationException(
                "Failed to lookup configurator for setup of handler: " + handler.getClass().getName(), e );
        }

        LOGGER.debug( "-- end configuration --" );
    }

    private void configureArchiver( final Archiver archiver, final AssemblerConfigurationSource configSource )
    {
        Xpp3Dom config;
        try
        {
            config = Xpp3DomBuilder.build( new StringReader( configSource.getArchiverConfig() ) );
        }
        catch ( final XmlPullParserException | IOException e )
        {
            throw new ArchiverException( "Failed to parse archiver configuration for: " + archiver.getClass().getName(),
                                         e );
        }

        LOGGER.debug( "Configuring archiver: '" + archiver.getClass().getName() + "' -->" );

        try
        {
            configureComponent( archiver, config, configSource );
        }
        catch ( final ComponentConfigurationException e )
        {
            throw new ArchiverException( "Failed to configure archiver: " + archiver.getClass().getName(), e );
        }
        catch ( final ComponentLookupException e )
        {
            throw new ArchiverException(
                "Failed to lookup configurator for setup of archiver: " + archiver.getClass().getName(), e );
        }

        LOGGER.debug( "-- end configuration --" );
    }

    private void configureComponent( final Object component, final Xpp3Dom config,
                                     final AssemblerConfigurationSource configSource )
        throws ComponentLookupException, ComponentConfigurationException
    {
        final ComponentConfigurator configurator = container.lookup( ComponentConfigurator.class, "basic" );

        final ConfigurationListener listener = new DebugConfigurationListener( LOGGER );

        final ExpressionEvaluator expressionEvaluator = new AssemblyExpressionEvaluator( configSource );

        final XmlPlexusConfiguration configuration = new XmlPlexusConfiguration( config );

        final Object[] containerRealm = getContainerRealm();

        /*
         * NOTE: The signature of configureComponent() has changed in Maven 3.x, the reflection prevents a linkage error
         * and makes the code work with both Maven 2 and 3.
         */
        try
        {
            final Method configureComponent =
                ComponentConfigurator.class.getMethod( "configureComponent", Object.class, PlexusConfiguration.class,
                                                       ExpressionEvaluator.class, (Class<?>) containerRealm[1],
                                                       ConfigurationListener.class );

            configureComponent.invoke( configurator, component, configuration, expressionEvaluator, containerRealm[0],
                                       listener );
        }
        catch ( final NoSuchMethodException | IllegalAccessException e )
        {
            throw new RuntimeException( e );
        }
        catch ( final InvocationTargetException e )
        {
            if ( e.getCause() instanceof ComponentConfigurationException )
            {
                throw (ComponentConfigurationException) e.getCause();
            }
            throw new RuntimeException( e.getCause() );
        }
    }

    private Object[] getContainerRealm()
    {
        /*
         * NOTE: The return type of getContainerRealm() has changed in Maven 3.x, the reflection prevents a linkage
         * error and makes the code work with both Maven 2 and 3.
         */
        try
        {
            final Method getContainerRealm = container.getClass().getMethod( "getContainerRealm" );
            return new Object[]{ getContainerRealm.invoke( container ), getContainerRealm.getReturnType() };
        }
        catch ( final NoSuchMethodException | IllegalAccessException e )
        {
            throw new RuntimeException( e );
        }
        catch ( final InvocationTargetException e )
        {
            throw new RuntimeException( e.getCause() );
        }
    }

    protected Archiver createWarArchiver()
        throws NoSuchArchiverException
    {
        final WarArchiver warArchiver = (WarArchiver) archiverManager.getArchiver( "war" );
        warArchiver.setIgnoreWebxml( false ); // See MNG-1274

        return warArchiver;
    }

    protected Archiver createTarArchiver( final String format, final TarLongFileMode tarLongFileMode )
        throws NoSuchArchiverException
    {
        final TarArchiver tarArchiver = (TarArchiver) archiverManager.getArchiver( "tar" );
        final int index = format.indexOf( '.' );
        if ( index >= 0 )
        {
            TarArchiver.TarCompressionMethod tarCompressionMethod;
            // TODO: this should accept gz and bz2 as well so we can skip
            // TODO: over the switch
            final String compression = format.substring( index + 1 );
            if ( "gz".equals( compression ) )
            {
                tarCompressionMethod = TarArchiver.TarCompressionMethod.gzip;
            }
            else if ( "bz2".equals( compression ) )
            {
                tarCompressionMethod = TarArchiver.TarCompressionMethod.bzip2;
            }
            else if ( "xz".equals( compression ) )
            {
                tarCompressionMethod = TarArchiver.TarCompressionMethod.xz;
            }
            else if ( "snappy".equals( compression ) )
            {
                tarCompressionMethod = TarArchiver.TarCompressionMethod.snappy;
            }
            else
            {
                // TODO: better handling
                throw new IllegalArgumentException( "Unknown compression format: " + compression );
            }
            tarArchiver.setCompression( tarCompressionMethod );
        }
        else if ( "tgz".equals( format ) )
        {
            tarArchiver.setCompression( TarArchiver.TarCompressionMethod.gzip );
        }
        else if ( "tbz2".equals( format ) )
        {
            tarArchiver.setCompression( TarArchiver.TarCompressionMethod.bzip2 );
        }
        else if ( "txz".equals( format ) )
        {
            tarArchiver.setCompression( TarArchiver.TarCompressionMethod.xz );
        }

        tarArchiver.setLongfile( tarLongFileMode );

        return tarArchiver;
    }
}
