package org.apache.maven.artifact.deployer;

/*
 * Copyright 2001-2005 The Apache Software Foundation.
 *
 * 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.
 */

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.maven.MavenConstants;
import org.apache.maven.MavenException;
import org.apache.maven.artifact.PomRewriter;
import org.apache.maven.project.Project;
import org.apache.maven.repository.ArtifactTypeHandler;
import org.apache.maven.repository.DefaultArtifactTypeHandler;
import org.apache.maven.wagon.ConnectionException;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.Wagon;
import org.apache.maven.wagon.authentication.AuthenticationException;
import org.apache.maven.wagon.authentication.AuthenticationInfo;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.events.TransferListener;
import org.apache.maven.wagon.observers.ChecksumObserver;
import org.apache.maven.wagon.providers.file.FileWagon;
import org.apache.maven.wagon.providers.ftp.FtpWagon;
import org.apache.maven.wagon.providers.http.HttpWagon;
import org.apache.maven.wagon.providers.ssh.ScpWagon;
import org.apache.maven.wagon.providers.ssh.SftpWagon;
import org.apache.maven.wagon.providers.sshext.ScpExternalWagon;
import org.apache.maven.wagon.repository.Repository;
import org.codehaus.plexus.util.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

/**
 * Default implementation of Artifact Deployer interface.
 *
 * @author <a href="mailto:michal.maczka@dimatics.com">Michal Maczka</a>
 * @version $Id$
 */
public class DefaultArtifactDeployer
    implements ArtifactDeployer
{

    private static final String POM_TYPE = "pom";

    private static final ArtifactTypeHandler POM_ARTIFACT_TYPE_HANDLER = new DefaultArtifactTypeHandler();

    /**
     * Date/time stamp which is appended to snapshot filenames
     */
    private final static String SNAPSHOT_FORMAT = "yyyyMMdd.HHmmss";

    private static final Log LOG = LogFactory.getLog( DefaultArtifactDeployer.class );

    /**
     * @see ArtifactDeployer#deploy(String, String, Project, ArtifactTypeHandler)
     */
    public void deploy( String artifact, String type, Project project, ArtifactTypeHandler handler )
        throws MavenException
    {
        handleDeploy( type, project, artifact, handler, project.getCurrentVersion() );
    }

    /**
     * @see DefaultArtifactDeployer#deploySnapshot(String, String, Project, ArtifactTypeHandler)
     */
    public void deploySnapshot( String artifact, String type, Project project, ArtifactTypeHandler handler )
        throws MavenException
    {
        handleDeploy( type, project, artifact, handler, MavenConstants.SNAPSHOT_SIGNIFIER );
    }

    private void handleDeploy( String type, Project project, String artifact, ArtifactTypeHandler handler,
                               String version )
        throws MavenException
    {
        File file;
        if ( POM_TYPE.equals( type ) )
        {
            file = PomRewriter.getRewrittenPom( project );
        }
        else
        {
            file = getFileForArtifact( artifact );
        }

        // do not deploy POM twice
        if ( !POM_TYPE.equals( type ) )
        {
            doDeploy( PomRewriter.getRewrittenPom( project ), project, POM_ARTIFACT_TYPE_HANDLER, version, POM_TYPE );
        }

        doDeploy( file, project, handler, version, type );
    }

    /**
     * @see ArtifactDeployer#install(String, String, Project, ArtifactTypeHandler)
     */
    public void install( String artifact, String type, Project project, ArtifactTypeHandler handler )
        throws MavenException
    {
        handleInstall( type, project, artifact, handler, project.getCurrentVersion() );
    }

    /**
     * @see ArtifactDeployer#installSnapshot(String, String, Project, ArtifactTypeHandler)
     */
    public void installSnapshot( String artifact, String type, Project project, ArtifactTypeHandler handler )
        throws MavenException
    {
        handleInstall( type, project, artifact, handler, MavenConstants.SNAPSHOT_SIGNIFIER );
    }

    private void handleInstall( String type, Project project, String artifact, ArtifactTypeHandler handler,
                                String version )
        throws MavenException
    {
        File file;
        if ( POM_TYPE.equals( type ) )
        {
            file = PomRewriter.getRewrittenPom( project );
        }
        else
        {
            file = getFileForArtifact( artifact );
        }

        doInstall( file, type, project, version, handler );

        // do not install twice
        if ( !POM_TYPE.equals( type ) )
        {
            doInstall( PomRewriter.getRewrittenPom( project ), POM_TYPE, project, version, POM_ARTIFACT_TYPE_HANDLER );
        }
    }

    /**
     * Install given file in local repository
     *
     * @param file the artifact file to install
     * @param type The type of the artiafct
     * @param project
     * @param version String denominating the version of the artifact
     * @throws MavenException
     */
    private void doInstall( File file, String type, Project project, String version, ArtifactTypeHandler handler )
        throws MavenException
    {
        try
        {
            Repository repository = new Repository( "local", "file:" + project.getContext().getMavenRepoLocal() );
            String repositoryPath = handler.constructRepositoryFullPath( type, project, version );
            deployFile( repository, file, repositoryPath, project );
        }
        catch ( Exception e )
        {
            String msg = "Cannot install file: '" + file + "'. Reason: " + e.getMessage();
            throw new MavenException( msg, e );
        }
    }

    private String findSshIdentity()
    {
        String key = findSshIdentity( System.getProperty( "user.home" ) );
        if ( key != null )
        {
            return key;
        }
        if ( System.getProperty( "user.home" ).equals( System.getProperty( "user.home.env" ) ) == false )
        {
            key = findSshIdentity( System.getProperty( "user.home.env" ) );
            if ( key != null )
            {
                return key;
            }
        }
        LOG.warn( "Unable to locate identity id_rsa, id_dsa or identity - set maven.repo.default.privatekey" );
        return null;
    }

    private String findSshIdentity( String home )
    {
        if ( home == null )
        {
            return null;
        }
        File sshHome = new File( home, ".ssh" );
        LOG.debug( "Looking for SSH keys in " + sshHome );
        File key = new File( sshHome, "id_dsa" );
        if ( key.exists() )
        {
            LOG.debug( "found " + key );
            return key.getAbsolutePath();
        }
        key = new File( sshHome, "id_rsa" );
        if ( key.exists() )
        {
            LOG.debug( "found " + key );
            return key.getAbsolutePath();
        }
        key = new File( sshHome, "identity" );
        if ( key.exists() )
        {
            LOG.debug( "found " + key );
            return key.getAbsolutePath();
        }
        return null;
    }

    private void doDeploy( File file, Project project, ArtifactTypeHandler handler, String version, String type )
        throws MavenException
    {
        List srcFiles = new ArrayList( 3 );
        List destFiles = new ArrayList( 3 );

        srcFiles.add( file );
        destFiles.add( handler.constructRepositoryFullPath( type, project, version ) );

        if ( version.indexOf( MavenConstants.SNAPSHOT_SIGNIFIER ) >= 0 )
        {
            String snapshotSignature = getSnapshotSignature();
            String v = StringUtils.replace( version, MavenConstants.SNAPSHOT_SIGNIFIER, snapshotSignature );

            File snapshotVersionFile = createSnapshotVersionFile( file, v, project, type );

            String snapshotVersionsFilename = handler.constructRepositoryDirectoryPath( type, project ) +
                project.getArtifactId() + "-snapshot-version";

            srcFiles.add( snapshotVersionFile );
            destFiles.add( snapshotVersionsFilename );

            srcFiles.add( file );
            destFiles.add( handler.constructRepositoryFullPath( type, project, v ) );
        }

        // trick add special values to context for default repository;

        String repoStr = (String) project.getContext().getVariable( "maven.repo.list" );

        if ( repoStr == null || repoStr.trim().length() == 0 )
        {
            String central = project.getDistributionSite();
            String centralDirectory = project.getDistributionDirectory();
            if ( central == null || central.trim().length() == 0 )
            {
                central = (String) project.getContext().getVariable( "maven.repo.central" );
                centralDirectory = (String) project.getContext().getVariable( "maven.repo.central.directory" );
            }
            if ( central != null && central.trim().length() > 0 )
            {
                repoStr = "default";
                project.getContext().setVariable( "maven.repo.default", "scp://" + central );
                if ( project.getContext().getVariable( "maven.repo.default.privatekey" ) == null )
                {
                    project.getContext().setVariable( "maven.repo.default.privatekey", findSshIdentity() );
                }
                if ( project.getContext().getVariable( "maven.repo.default.passphrase" ) == null )
                {
                    LOG.warn( "WARNING: assuming empty passphrase. Specify maven.repo.default.passphrase if needed" );
                    project.getContext().setVariable( "maven.repo.default.passphrase", "" );
                }
                project.getContext().setVariable( "maven.repo.default.directory", centralDirectory );
                project.getContext().setVariable( "maven.repo.default.username",
                                                  project.getContext().getVariable( "maven.username" ) );
                project.getContext().setVariable( "maven.repo.default.group",
                                                  project.getContext().getVariable( "maven.remote.group" ) );
            }
        }

        String[] repos = StringUtils.split( repoStr, "," );

        LOG.info( "Will deploy to " + repos.length + " repository(ies): " + repoStr );
        boolean success = false;
        for ( int i = 0; i < repos.length; i++ )
        {

            String repo = repos[i].trim();
            LOG.info( "Deploying to repository: " + repo );
            Repository repository = RepositoryBuilder.getRepository( project, repo );
            AuthenticationInfo authenticationInfo = RepositoryBuilder.getAuthenticationInfo( project, repo );
            try
            {
                deployFiles( repository, srcFiles, destFiles, authenticationInfo, project );
                success = true;
            }
            catch ( Exception e )
            {
                String msg = "Failed to deploy to: " + repository.getId() + " Reason: " + e;
                LOG.warn( msg, e );
                // deploy to next repository
                continue;
            }

        }
        if ( !success )
        {
            throw new MavenException( "Unable to deploy to any repositories" );
        }
    }

    private void deployFile( Repository repository, File src, String dest, Project project )
        throws ResourceDoesNotExistException, MalformedURLException, NoSuchAlgorithmException, TransferFailedException,
        ConnectionException, AuthenticationException, AuthorizationException, MavenException
    {
        deployFiles( repository, Collections.singletonList( src ), Collections.singletonList( dest ), null, project );
    }

    private void deployFiles( Repository repository, List srcFiles, List destFiles,
                              AuthenticationInfo authenticationInfo, Project project )
        throws ConnectionException, AuthenticationException, ResourceDoesNotExistException, TransferFailedException,
        AuthorizationException, MalformedURLException, NoSuchAlgorithmException, MavenException
    {

        if ( srcFiles.size() != destFiles.size() )
        {
            String msg = "Lengths of the lists should be the same";
            throw new IllegalArgumentException( msg );
        }

        Wagon wagon = getWagon( repository.getProtocol(), project, repository.getId() );

        TransferListener uploadMonitor = new UploadMeter();

        Map checksums = new HashMap( 2 );

        ChecksumObserver observer = new ChecksumObserver( "MD5" );
        checksums.put( "md5", observer );
        wagon.addTransferListener( observer );
        observer = new ChecksumObserver( "SHA-1" );
        checksums.put( "sha1", observer );
        wagon.addTransferListener( observer );

        try
        {
            wagon.connect( repository, authenticationInfo );
            Iterator srcIterator = srcFiles.iterator();
            Iterator destIterator = destFiles.iterator();
            while ( srcIterator.hasNext() )
            {
                wagon.addTransferListener( uploadMonitor );
 
                File srcFile = (File) srcIterator.next();
                String destFile = (String) destIterator.next();
                wagon.put( srcFile, destFile );

                wagon.removeTransferListener( uploadMonitor );

                Map sums = new HashMap( 2 );
                for ( Iterator i = checksums.keySet().iterator(); i.hasNext(); )
                {
                    // store first - a later put will modify them
                    String extension = (String) i.next();
                    observer = (ChecksumObserver) checksums.get( extension );
                    sums.put( extension, observer.getActualChecksum() );
                }

                for ( Iterator i = checksums.keySet().iterator(); i.hasNext(); )
                {
                    String extension = (String) i.next();
    
                    // TODO: shouldn't need a file intermediatary - improve wagon to take a stream
                    File temp = File.createTempFile( "maven-artifact", null );
                    temp.deleteOnExit();
                    FileUtils.fileWrite( temp.getAbsolutePath(), (String) sums.get( extension ) );
    
                    wagon.put( temp, destFile + "." + extension );
                }
            }
        }
        catch ( IOException e )
        {
            throw new MavenException( "Error creating temporary file to transfer checksums", e );
        }
        finally
        {
            try
            {
                wagon.disconnect();
            }
            catch ( Exception e )
            {
                LOG.error( "Error cleaning up from the deployer", e );
            }
        }
    }

    private Wagon getWagon( String protocol, Project project, String id )
        throws MalformedURLException
    {
        if ( protocol.equals( "http" ) )
        {
            return new HttpWagon();
        }
        else if ( protocol.equals( "ftp" ) )
        {
            FtpWagon ftpWagon = new FtpWagon();
            RepositoryBuilder.configureFtpWagon( project, id, ftpWagon );
            return ftpWagon;
        }
        else if ( protocol.equals( "sftp" ) )
        {
            return new SftpWagon();
        }
        else if ( protocol.equals( "file" ) )
        {
            return new FileWagon();
        }
        else if ( protocol.equals( "scp" ) )
        {
            return new ScpWagon();
        }
        else if ( protocol.equals( "scpexe" ) )
        {
            ScpExternalWagon scpExternalWagon = new ScpExternalWagon();
            RepositoryBuilder.configureSshExternalWagon( project, id, scpExternalWagon );
            return scpExternalWagon;
        }
        else
        {
            throw new MalformedURLException( "Unknown Wagon protocol: " + protocol );
        }
    }

    private String getSnapshotSignature()
    {
        DateFormat fmt = new SimpleDateFormat( SNAPSHOT_FORMAT );
        fmt.setTimeZone( TimeZone.getTimeZone( "GMT" ) );
        return fmt.format( new Date() );
    }

    private File getFileForArtifact( String artifact )
        throws MavenException
    {
        File file = new File( artifact );
        if ( !file.exists() )
        {
            String msg = "Artifact file: '" + artifact + "' must exist";
            throw new MavenException( msg );
        }
        if ( !file.canRead() )
        {
            String msg = "Artifact file: '" + artifact + "' must be readable";
            throw new MavenException( msg );
        }
        if ( file.isDirectory() )
        {
            String msg = "Artifact file: '" + artifact + "' must not be a directory";
            throw new MavenException( msg );
        }
        return file.getAbsoluteFile();
    }

    /**
     * Create a file which contains timestamp of the latetst snapshot
     */
    private File createSnapshotVersionFile( File artifact, String snapshotVersion, Project project, String type )
        throws MavenException
    {
        File file = null;
        String filename = project.getArtifactId() + "-" + type + "-snapshot-version";
        try
        {
            file = new File( artifact.getParent(), filename );
            FileUtils.fileWrite( file.getAbsolutePath(), snapshotVersion );
            file.deleteOnExit();
        }
        catch ( Exception e )
        {
            throw new MavenException( "Cannot create snapshot-version file:" + file );
        }
        return file;
    }

}
