package org.eclipse.aether.internal.impl;

/*
 * 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.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.List;
import static java.util.Objects.requireNonNull;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Named;

import org.eclipse.aether.RepositoryEvent;
import org.eclipse.aether.RepositoryEvent.EventType;
import org.eclipse.aether.RepositoryException;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.RequestTrace;
import org.eclipse.aether.SyncContext;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.deployment.DeployRequest;
import org.eclipse.aether.deployment.DeployResult;
import org.eclipse.aether.deployment.DeploymentException;
import org.eclipse.aether.impl.Deployer;
import org.eclipse.aether.impl.MetadataGenerator;
import org.eclipse.aether.impl.MetadataGeneratorFactory;
import org.eclipse.aether.impl.OfflineController;
import org.eclipse.aether.impl.RemoteRepositoryManager;
import org.eclipse.aether.impl.RepositoryConnectorProvider;
import org.eclipse.aether.impl.RepositoryEventDispatcher;
import org.eclipse.aether.impl.SyncContextFactory;
import org.eclipse.aether.impl.UpdateCheck;
import org.eclipse.aether.impl.UpdateCheckManager;
import org.eclipse.aether.metadata.MergeableMetadata;
import org.eclipse.aether.metadata.Metadata;
import org.eclipse.aether.repository.LocalRepositoryManager;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.RepositoryPolicy;
import org.eclipse.aether.spi.connector.ArtifactUpload;
import org.eclipse.aether.spi.connector.MetadataDownload;
import org.eclipse.aether.spi.connector.MetadataUpload;
import org.eclipse.aether.spi.connector.RepositoryConnector;
import org.eclipse.aether.spi.io.FileProcessor;
import org.eclipse.aether.spi.locator.Service;
import org.eclipse.aether.spi.locator.ServiceLocator;
import org.eclipse.aether.transfer.ArtifactTransferException;
import org.eclipse.aether.transfer.MetadataNotFoundException;
import org.eclipse.aether.transfer.MetadataTransferException;
import org.eclipse.aether.transfer.NoRepositoryConnectorException;
import org.eclipse.aether.transfer.RepositoryOfflineException;
import org.eclipse.aether.transfer.TransferCancelledException;
import org.eclipse.aether.transfer.TransferEvent;
import org.eclipse.aether.transform.FileTransformer;
import org.eclipse.aether.transform.FileTransformerManager;

/**
 */
@Named
public class DefaultDeployer
    implements Deployer, Service
{
    private FileProcessor fileProcessor;

    private RepositoryEventDispatcher repositoryEventDispatcher;

    private RepositoryConnectorProvider repositoryConnectorProvider;

    private RemoteRepositoryManager remoteRepositoryManager;

    private UpdateCheckManager updateCheckManager;

    private Collection<MetadataGeneratorFactory> metadataFactories = new ArrayList<>();

    private SyncContextFactory syncContextFactory;

    private OfflineController offlineController;

    public DefaultDeployer()
    {
        // enables default constructor
    }

    @SuppressWarnings( "checkstyle:parameternumber" )
    @Inject
    DefaultDeployer( FileProcessor fileProcessor, RepositoryEventDispatcher repositoryEventDispatcher,
                     RepositoryConnectorProvider repositoryConnectorProvider,
                     RemoteRepositoryManager remoteRepositoryManager, UpdateCheckManager updateCheckManager,
                     Set<MetadataGeneratorFactory> metadataFactories, SyncContextFactory syncContextFactory,
                     OfflineController offlineController )
    {
        setFileProcessor( fileProcessor );
        setRepositoryEventDispatcher( repositoryEventDispatcher );
        setRepositoryConnectorProvider( repositoryConnectorProvider );
        setRemoteRepositoryManager( remoteRepositoryManager );
        setUpdateCheckManager( updateCheckManager );
        setMetadataGeneratorFactories( metadataFactories );
        setSyncContextFactory( syncContextFactory );
        setOfflineController( offlineController );
    }

    public void initService( ServiceLocator locator )
    {
        setFileProcessor( locator.getService( FileProcessor.class ) );
        setRepositoryEventDispatcher( locator.getService( RepositoryEventDispatcher.class ) );
        setRepositoryConnectorProvider( locator.getService( RepositoryConnectorProvider.class ) );
        setRemoteRepositoryManager( locator.getService( RemoteRepositoryManager.class ) );
        setUpdateCheckManager( locator.getService( UpdateCheckManager.class ) );
        setMetadataGeneratorFactories( locator.getServices( MetadataGeneratorFactory.class ) );
        setSyncContextFactory( locator.getService( SyncContextFactory.class ) );
        setOfflineController( locator.getService( OfflineController.class ) );
    }

    public DefaultDeployer setFileProcessor( FileProcessor fileProcessor )
    {
        this.fileProcessor = requireNonNull( fileProcessor, "file processor cannot be null" );
        return this;
    }

    public DefaultDeployer setRepositoryEventDispatcher( RepositoryEventDispatcher repositoryEventDispatcher )
    {
        this.repositoryEventDispatcher = requireNonNull(
                repositoryEventDispatcher, "repository event dispatcher cannot be null" );
        return this;
    }

    public DefaultDeployer setRepositoryConnectorProvider( RepositoryConnectorProvider repositoryConnectorProvider )
    {
        this.repositoryConnectorProvider = requireNonNull(
                repositoryConnectorProvider, "repository connector provider cannot be null" );
        return this;
    }

    public DefaultDeployer setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
    {
        this.remoteRepositoryManager = requireNonNull(
                remoteRepositoryManager, "remote repository provider cannot be null" );
        return this;
    }

    public DefaultDeployer setUpdateCheckManager( UpdateCheckManager updateCheckManager )
    {
        this.updateCheckManager = requireNonNull( updateCheckManager, "update check manager cannot be null" );
        return this;
    }

    public DefaultDeployer addMetadataGeneratorFactory( MetadataGeneratorFactory factory )
    {
        metadataFactories.add( requireNonNull( factory, "metadata generator factory cannot be null" ) );
        return this;
    }

    public DefaultDeployer setMetadataGeneratorFactories( Collection<MetadataGeneratorFactory> metadataFactories )
    {
        if ( metadataFactories == null )
        {
            this.metadataFactories = new ArrayList<>();
        }
        else
        {
            this.metadataFactories = metadataFactories;
        }
        return this;
    }

    public DefaultDeployer setSyncContextFactory( SyncContextFactory syncContextFactory )
    {
        this.syncContextFactory = requireNonNull( syncContextFactory, "sync context factory cannot be null" );
        return this;
    }

    public DefaultDeployer setOfflineController( OfflineController offlineController )
    {
        this.offlineController = requireNonNull( offlineController, "offline controller cannot be null" );
        return this;
    }

    public DeployResult deploy( RepositorySystemSession session, DeployRequest request )
        throws DeploymentException
    {
        try
        {
            Utils.checkOffline( session, offlineController, request.getRepository() );
        }
        catch ( RepositoryOfflineException e )
        {
            throw new DeploymentException( "Cannot deploy while " + request.getRepository().getId() + " ("
                + request.getRepository().getUrl() + ") is in offline mode", e );
        }

        try ( SyncContext syncContext = syncContextFactory.newInstance( session, false ) )
        {
            return deploy( syncContext, session, request );
        }
    }

    private DeployResult deploy( SyncContext syncContext, RepositorySystemSession session, DeployRequest request )
        throws DeploymentException
    {
        DeployResult result = new DeployResult( request );

        RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );

        RemoteRepository repository = request.getRepository();

        RepositoryConnector connector;
        try
        {
            connector = repositoryConnectorProvider.newRepositoryConnector( session, repository );
        }
        catch ( NoRepositoryConnectorException e )
        {
            throw new DeploymentException( "Failed to deploy artifacts/metadata: " + e.getMessage(), e );
        }

        try
        {
            List<? extends MetadataGenerator> generators = getMetadataGenerators( session, request );

            FileTransformerManager fileTransformerManager = session.getFileTransformerManager();

            List<ArtifactUpload> artifactUploads = new ArrayList<>();
            List<MetadataUpload> metadataUploads = new ArrayList<>();
            IdentityHashMap<Metadata, Object> processedMetadata = new IdentityHashMap<>();

            EventCatapult catapult = new EventCatapult( session, trace, repository, repositoryEventDispatcher );

            List<Artifact> artifacts = new ArrayList<>( request.getArtifacts() );

            List<Metadata> metadatas = Utils.prepareMetadata( generators, artifacts );

            syncContext.acquire( artifacts, Utils.combine( request.getMetadata(), metadatas ) );

            for ( Metadata metadata : metadatas )
            {
                upload( metadataUploads, session, metadata, repository, connector, catapult );
                processedMetadata.put( metadata, null );
            }

            for ( int i = 0; i < artifacts.size(); i++ )
            {
                Artifact artifact = artifacts.get( i );

                for ( MetadataGenerator generator : generators )
                {
                    artifact = generator.transformArtifact( artifact );
                }

                artifacts.set( i, artifact );

                Collection<FileTransformer> fileTransformers =
                        fileTransformerManager.getTransformersForArtifact( artifact );
                if ( !fileTransformers.isEmpty() )
                {
                    for ( FileTransformer fileTransformer : fileTransformers )
                    {
                        Artifact targetArtifact = fileTransformer.transformArtifact( artifact );

                        ArtifactUpload upload = new ArtifactUpload( targetArtifact, artifact.getFile(),
                                fileTransformer );
                        upload.setTrace( trace );
                        upload.setListener( new ArtifactUploadListener( catapult, upload ) );
                        artifactUploads.add( upload );
                    }
                }
                else
                {
                    ArtifactUpload upload = new ArtifactUpload( artifact, artifact.getFile() );
                    upload.setTrace( trace );
                    upload.setListener( new ArtifactUploadListener( catapult, upload ) );
                    artifactUploads.add( upload );
                }
            }

            connector.put( artifactUploads, null );

            for ( ArtifactUpload upload : artifactUploads )
            {
                if ( upload.getException() != null )
                {
                    throw new DeploymentException( "Failed to deploy artifacts: " + upload.getException().getMessage(),
                                                   upload.getException() );
                }
                result.addArtifact( upload.getArtifact() );
            }

            metadatas = Utils.finishMetadata( generators, artifacts );

            syncContext.acquire( null, metadatas );

            for ( Metadata metadata : metadatas )
            {
                upload( metadataUploads, session, metadata, repository, connector, catapult );
                processedMetadata.put( metadata, null );
            }

            for ( Metadata metadata : request.getMetadata() )
            {
                if ( !processedMetadata.containsKey( metadata ) )
                {
                    upload( metadataUploads, session, metadata, repository, connector, catapult );
                    processedMetadata.put( metadata, null );
                }
            }

            connector.put( null, metadataUploads );

            for ( MetadataUpload upload : metadataUploads )
            {
                if ( upload.getException() != null )
                {
                    throw new DeploymentException( "Failed to deploy metadata: " + upload.getException().getMessage(),
                                                   upload.getException() );
                }
                result.addMetadata( upload.getMetadata() );
            }
        }
        finally
        {
            connector.close();
        }

        return result;
    }

    private List<? extends MetadataGenerator> getMetadataGenerators( RepositorySystemSession session,
                                                                     DeployRequest request )
    {
        PrioritizedComponents<MetadataGeneratorFactory> factories =
            Utils.sortMetadataGeneratorFactories( session, this.metadataFactories );

        List<MetadataGenerator> generators = new ArrayList<>();

        for ( PrioritizedComponent<MetadataGeneratorFactory> factory : factories.getEnabled() )
        {
            MetadataGenerator generator = factory.getComponent().newInstance( session, request );
            if ( generator != null )
            {
                generators.add( generator );
            }
        }

        return generators;
    }

    private void upload( Collection<MetadataUpload> metadataUploads, RepositorySystemSession session,
                         Metadata metadata, RemoteRepository repository, RepositoryConnector connector,
                         EventCatapult catapult )
        throws DeploymentException
    {
        LocalRepositoryManager lrm = session.getLocalRepositoryManager();
        File basedir = lrm.getRepository().getBasedir();

        File dstFile = new File( basedir, lrm.getPathForRemoteMetadata( metadata, repository, "" ) );

        if ( metadata instanceof MergeableMetadata )
        {
            if ( !( (MergeableMetadata) metadata ).isMerged() )
            {
                RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_RESOLVING );
                event.setTrace( catapult.getTrace() );
                event.setMetadata( metadata );
                event.setRepository( repository );
                repositoryEventDispatcher.dispatch( event.build() );

                event = new RepositoryEvent.Builder( session, EventType.METADATA_DOWNLOADING );
                event.setTrace( catapult.getTrace() );
                event.setMetadata( metadata );
                event.setRepository( repository );
                repositoryEventDispatcher.dispatch( event.build() );

                RepositoryPolicy policy = getPolicy( session, repository, metadata.getNature() );
                MetadataDownload download = new MetadataDownload();
                download.setMetadata( metadata );
                download.setFile( dstFile );
                download.setChecksumPolicy( policy.getChecksumPolicy() );
                download.setListener( SafeTransferListener.wrap( session ) );
                download.setTrace( catapult.getTrace() );
                connector.get( null, Arrays.asList( download ) );

                Exception error = download.getException();

                if ( error instanceof MetadataNotFoundException )
                {
                    dstFile.delete();
                }

                event = new RepositoryEvent.Builder( session, EventType.METADATA_DOWNLOADED );
                event.setTrace( catapult.getTrace() );
                event.setMetadata( metadata );
                event.setRepository( repository );
                event.setException( error );
                event.setFile( dstFile );
                repositoryEventDispatcher.dispatch( event.build() );

                event = new RepositoryEvent.Builder( session, EventType.METADATA_RESOLVED );
                event.setTrace( catapult.getTrace() );
                event.setMetadata( metadata );
                event.setRepository( repository );
                event.setException( error );
                event.setFile( dstFile );
                repositoryEventDispatcher.dispatch( event.build() );

                if ( error != null && !( error instanceof MetadataNotFoundException ) )
                {
                    throw new DeploymentException( "Failed to retrieve remote metadata " + metadata + ": "
                        + error.getMessage(), error );
                }
            }

            try
            {
                ( (MergeableMetadata) metadata ).merge( dstFile, dstFile );
            }
            catch ( RepositoryException e )
            {
                throw new DeploymentException( "Failed to update metadata " + metadata + ": " + e.getMessage(), e );
            }
        }
        else
        {
            if ( metadata.getFile() == null )
            {
                throw new DeploymentException( "Failed to update metadata " + metadata + ": No file attached." );
            }
            try
            {
                fileProcessor.copy( metadata.getFile(), dstFile );
            }
            catch ( IOException e )
            {
                throw new DeploymentException( "Failed to update metadata " + metadata + ": " + e.getMessage(), e );
            }
        }

        UpdateCheck<Metadata, MetadataTransferException> check = new UpdateCheck<>();
        check.setItem( metadata );
        check.setFile( dstFile );
        check.setRepository( repository );
        check.setAuthoritativeRepository( repository );
        updateCheckManager.touchMetadata( session, check );

        MetadataUpload upload = new MetadataUpload( metadata, dstFile );
        upload.setTrace( catapult.getTrace() );
        upload.setListener( new MetadataUploadListener( catapult, upload ) );
        metadataUploads.add( upload );
    }

    private RepositoryPolicy getPolicy( RepositorySystemSession session, RemoteRepository repository,
                                        Metadata.Nature nature )
    {
        boolean releases = !Metadata.Nature.SNAPSHOT.equals( nature );
        boolean snapshots = !Metadata.Nature.RELEASE.equals( nature );
        return remoteRepositoryManager.getPolicy( session, repository, releases, snapshots );
    }

    static final class EventCatapult
    {

        private final RepositorySystemSession session;

        private final RequestTrace trace;

        private final RemoteRepository repository;

        private final RepositoryEventDispatcher dispatcher;

        EventCatapult( RepositorySystemSession session, RequestTrace trace, RemoteRepository repository,
                              RepositoryEventDispatcher dispatcher )
        {
            this.session = session;
            this.trace = trace;
            this.repository = repository;
            this.dispatcher = dispatcher;
        }

        public RepositorySystemSession getSession()
        {
            return session;
        }

        public RequestTrace getTrace()
        {
            return trace;
        }

        public void artifactDeploying( Artifact artifact, File file )
        {
            RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.ARTIFACT_DEPLOYING );
            event.setTrace( trace );
            event.setArtifact( artifact );
            event.setRepository( repository );
            event.setFile( file );

            dispatcher.dispatch( event.build() );
        }

        public void artifactDeployed( Artifact artifact, File file, ArtifactTransferException exception )
        {
            RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.ARTIFACT_DEPLOYED );
            event.setTrace( trace );
            event.setArtifact( artifact );
            event.setRepository( repository );
            event.setFile( file );
            event.setException( exception );

            dispatcher.dispatch( event.build() );
        }

        public void metadataDeploying( Metadata metadata, File file )
        {
            RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_DEPLOYING );
            event.setTrace( trace );
            event.setMetadata( metadata );
            event.setRepository( repository );
            event.setFile( file );

            dispatcher.dispatch( event.build() );
        }

        public void metadataDeployed( Metadata metadata, File file, Exception exception )
        {
            RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_DEPLOYED );
            event.setTrace( trace );
            event.setMetadata( metadata );
            event.setRepository( repository );
            event.setFile( file );
            event.setException( exception );

            dispatcher.dispatch( event.build() );
        }

    }

    static final class ArtifactUploadListener
        extends SafeTransferListener
    {

        private final EventCatapult catapult;

        private final ArtifactUpload transfer;

        ArtifactUploadListener( EventCatapult catapult, ArtifactUpload transfer )
        {
            super( catapult.getSession() );
            this.catapult = catapult;
            this.transfer = transfer;
        }

        @Override
        public void transferInitiated( TransferEvent event )
            throws TransferCancelledException
        {
            super.transferInitiated( event );
            catapult.artifactDeploying( transfer.getArtifact(), transfer.getFile() );
        }

        @Override
        public void transferFailed( TransferEvent event )
        {
            super.transferFailed( event );
            catapult.artifactDeployed( transfer.getArtifact(), transfer.getFile(), transfer.getException() );
        }

        @Override
        public void transferSucceeded( TransferEvent event )
        {
            super.transferSucceeded( event );
            catapult.artifactDeployed( transfer.getArtifact(), transfer.getFile(), null );
        }

    }

    static final class MetadataUploadListener
        extends SafeTransferListener
    {

        private final EventCatapult catapult;

        private final MetadataUpload transfer;

        MetadataUploadListener( EventCatapult catapult, MetadataUpload transfer )
        {
            super( catapult.getSession() );
            this.catapult = catapult;
            this.transfer = transfer;
        }

        @Override
        public void transferInitiated( TransferEvent event )
            throws TransferCancelledException
        {
            super.transferInitiated( event );
            catapult.metadataDeploying( transfer.getMetadata(), transfer.getFile() );
        }

        @Override
        public void transferFailed( TransferEvent event )
        {
            super.transferFailed( event );
            catapult.metadataDeployed( transfer.getMetadata(), transfer.getFile(), transfer.getException() );
        }

        @Override
        public void transferSucceeded( TransferEvent event )
        {
            super.transferSucceeded( event );
            catapult.metadataDeployed( transfer.getMetadata(), transfer.getFile(), null );
        }

    }

}