package org.apache.maven.wagon.providers.ftp;

/*
 * 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.commons.io.IOUtils;
import org.apache.commons.net.ProtocolCommandEvent;
import org.apache.commons.net.ProtocolCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.maven.wagon.ConnectionException;
import org.apache.maven.wagon.InputData;
import org.apache.maven.wagon.OutputData;
import org.apache.maven.wagon.PathUtils;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.StreamWagon;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.WagonConstants;
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.repository.RepositoryPermissions;
import org.apache.maven.wagon.resource.Resource;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

/**
 * FtpWagon
 *
 *
 * @plexus.component role="org.apache.maven.wagon.Wagon"
 * role-hint="ftp"
 * instantiation-strategy="per-lookup"
 */
public class FtpWagon
    extends StreamWagon
{
    private FTPClient ftp;

    /**
     * @plexus.configuration default-value="true"
     */
    private boolean passiveMode = true;

    /**
     * @plexus.configuration default-value="ISO-8859-1"
     */
    private String controlEncoding = FTP.DEFAULT_CONTROL_ENCODING;

    public boolean isPassiveMode()
    {
        return passiveMode;
    }

    public void setPassiveMode( boolean passiveMode )
    {
        this.passiveMode = passiveMode;
    }

    protected void openConnectionInternal()
        throws ConnectionException, AuthenticationException
    {
        AuthenticationInfo authInfo = getAuthenticationInfo();

        if ( authInfo == null )
        {
            throw new IllegalArgumentException( "Authentication Credentials cannot be null for FTP protocol" );
        }

        if ( authInfo.getUserName() == null )
        {
            authInfo.setUserName( System.getProperty( "user.name" ) );
        }

        String username = authInfo.getUserName();

        String password = authInfo.getPassword();

        if ( username == null )
        {
            throw new AuthenticationException( "Username not specified for repository " + getRepository().getId() );
        }
        if ( password == null )
        {
            throw new AuthenticationException( "Password not specified for repository " + getRepository().getId() );
        }

        String host = getRepository().getHost();

        ftp = new FTPClient();
        ftp.setDefaultTimeout( getTimeout() );
        ftp.setDataTimeout( getTimeout() );
        ftp.setControlEncoding( getControlEncoding() );

        ftp.addProtocolCommandListener( new PrintCommandListener( this ) );

        try
        {
            if ( getRepository().getPort() != WagonConstants.UNKNOWN_PORT )
            {
                ftp.connect( host, getRepository().getPort() );
            }
            else
            {
                ftp.connect( host );
            }

            // After connection attempt, you should check the reply code to
            // verify
            // success.
            int reply = ftp.getReplyCode();

            if ( !FTPReply.isPositiveCompletion( reply ) )
            {
                ftp.disconnect();

                throw new AuthenticationException( "FTP server refused connection." );
            }
        }
        catch ( IOException e )
        {
            if ( ftp.isConnected() )
            {
                try
                {
                    fireSessionError( e );

                    ftp.disconnect();
                }
                catch ( IOException f )
                {
                    // do nothing
                }
            }

            throw new AuthenticationException( "Could not connect to server.", e );
        }

        try
        {
            if ( !ftp.login( username, password ) )
            {
                throw new AuthenticationException( "Cannot login to remote system" );
            }

            fireSessionDebug( "Remote system is " + ftp.getSystemName() );

            // Set to binary mode.
            ftp.setFileType( FTP.BINARY_FILE_TYPE );
            ftp.setListHiddenFiles( true );

            // Use passive mode as default because most of us are
            // behind firewalls these days.
            if ( isPassiveMode() )
            {
                ftp.enterLocalPassiveMode();
            }
        }
        catch ( IOException e )
        {
            throw new ConnectionException( "Cannot login to remote system", e );
        }
    }

    protected void firePutCompleted( Resource resource, File file )
    {
        try
        {
            // TODO [BP]: verify the order is correct
            ftp.completePendingCommand();

            RepositoryPermissions permissions = repository.getPermissions();

            if ( permissions != null && permissions.getGroup() != null )
            {
                // ignore failures
                ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + resource.getName() );
            }

            if ( permissions != null && permissions.getFileMode() != null )
            {
                // ignore failures
                ftp.sendSiteCommand( "CHMOD " + permissions.getFileMode() + " " + resource.getName() );
            }
        }
        catch ( IOException e )
        {
            // TODO: handle
            // michal I am not sure  what error means in that context
            // I think that we will be able to recover or simply we will fail later on
        }

        super.firePutCompleted( resource, file );
    }

    protected void fireGetCompleted( Resource resource, File localFile )
    {
        try
        {
            ftp.completePendingCommand();
        }
        catch ( IOException e )
        {
            // TODO: handle
            // michal I am not sure  what error means in that context
            // actually I am not even sure why we have to invoke that command
            // I think that we will be able to recover or simply we will fail later on
        }
        super.fireGetCompleted( resource, localFile );
    }

    public void closeConnection()
        throws ConnectionException
    {
        if ( ftp != null && ftp.isConnected() )
        {
            try
            {
                // This is a NPE rethink shutting down the streams
                ftp.disconnect();
            }
            catch ( IOException e )
            {
                throw new ConnectionException( "Failed to close connection to FTP repository", e );
            }
        }
    }

    public void fillOutputData( OutputData outputData )
        throws TransferFailedException
    {
        OutputStream os;

        Resource resource = outputData.getResource();

        RepositoryPermissions permissions = repository.getPermissions();

        try
        {
            if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
            {
                throw new TransferFailedException(
                    "Required directory: '" + getRepository().getBasedir() + "' " + "is missing" );
            }

            String[] dirs = PathUtils.dirnames( resource.getName() );

            for ( String dir : dirs )
            {
                boolean dirChanged = ftp.changeWorkingDirectory( dir );

                if ( !dirChanged )
                {
                    // first, try to create it
                    boolean success = ftp.makeDirectory( dir );

                    if ( success )
                    {
                        if ( permissions != null && permissions.getGroup() != null )
                        {
                            // ignore failures
                            ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + dir );
                        }

                        if ( permissions != null && permissions.getDirectoryMode() != null )
                        {
                            // ignore failures
                            ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() + " " + dir );
                        }

                        dirChanged = ftp.changeWorkingDirectory( dir );
                    }
                }

                if ( !dirChanged )
                {
                    throw new TransferFailedException( "Unable to create directory " + dir );
                }
            }

            // we come back to original basedir so
            // FTP wagon is ready for next requests
            if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
            {
                throw new TransferFailedException( "Unable to return to the base directory" );
            }

            os = ftp.storeFileStream( resource.getName() );

            if ( os == null )
            {
                String msg =
                    "Cannot transfer resource:  '" + resource + "'. Output stream is null. FTP Server response: "
                        + ftp.getReplyString();

                throw new TransferFailedException( msg );

            }

            fireTransferDebug( "resource = " + resource );

        }
        catch ( IOException e )
        {
            throw new TransferFailedException( "Error transferring over FTP", e );
        }

        outputData.setOutputStream( os );

    }

    // ----------------------------------------------------------------------
    //
    // ----------------------------------------------------------------------

    public void fillInputData( InputData inputData )
        throws TransferFailedException, ResourceDoesNotExistException
    {
        InputStream is;

        Resource resource = inputData.getResource();

        try
        {
            ftpChangeDirectory( resource );

            String filename = PathUtils.filename( resource.getName() );
            FTPFile[] ftpFiles = ftp.listFiles( filename );

            if ( ftpFiles == null || ftpFiles.length <= 0 )
            {
                throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" );
            }

            long contentLength = ftpFiles[0].getSize();

            //@todo check how it works! javadoc of common login says:
            // Returns the file timestamp. This usually the last modification time.
            //
            Calendar timestamp = ftpFiles[0].getTimestamp();
            long lastModified = timestamp != null ? timestamp.getTimeInMillis() : 0;

            resource.setContentLength( contentLength );

            resource.setLastModified( lastModified );

            is = ftp.retrieveFileStream( filename );
        }
        catch ( IOException e )
        {
            throw new TransferFailedException( "Error transferring file via FTP", e );
        }

        inputData.setInputStream( is );
    }

    private void ftpChangeDirectory( Resource resource )
        throws IOException, TransferFailedException, ResourceDoesNotExistException
    {
        if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
        {
            throw new ResourceDoesNotExistException(
                "Required directory: '" + getRepository().getBasedir() + "' " + "is missing" );
        }

        String[] dirs = PathUtils.dirnames( resource.getName() );

        for ( String dir : dirs )
        {
            boolean dirChanged = ftp.changeWorkingDirectory( dir );

            if ( !dirChanged )
            {
                String msg = "Resource " + resource + " not found. Directory " + dir + " does not exist";

                throw new ResourceDoesNotExistException( msg );
            }
        }
    }

    /**
     * 
     */
    public class PrintCommandListener
        implements ProtocolCommandListener
    {
        private FtpWagon wagon;

        public PrintCommandListener( FtpWagon wagon )
        {
            this.wagon = wagon;
        }

        public void protocolCommandSent( ProtocolCommandEvent event )
        {
            wagon.fireSessionDebug( "Command sent: " + event.getMessage() );

        }

        public void protocolReplyReceived( ProtocolCommandEvent event )
        {
            wagon.fireSessionDebug( "Reply received: " + event.getMessage() );
        }
    }

    protected void fireSessionDebug( String msg )
    {
        super.fireSessionDebug( msg );
    }

    public List<String> getFileList( String destinationDirectory )
        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
    {
        Resource resource = new Resource( destinationDirectory );

        try
        {
            ftpChangeDirectory( resource );

            String filename = PathUtils.filename( resource.getName() );
            FTPFile[] ftpFiles = ftp.listFiles( filename );

            if ( ftpFiles == null || ftpFiles.length <= 0 )
            {
                throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" );
            }

            List<String> ret = new ArrayList<String>();
            for ( FTPFile file : ftpFiles )
            {
                String name = file.getName();

                if ( file.isDirectory() && !name.endsWith( "/" ) )
                {
                    name += "/";
                }

                ret.add( name );
            }

            return ret;
        }
        catch ( IOException e )
        {
            throw new TransferFailedException( "Error transferring file via FTP", e );
        }
    }

    public boolean resourceExists( String resourceName )
        throws TransferFailedException, AuthorizationException
    {
        Resource resource = new Resource( resourceName );

        try
        {
            ftpChangeDirectory( resource );

            String filename = PathUtils.filename( resource.getName() );
            int status = ftp.stat( filename );

            return ( ( status == FTPReply.FILE_STATUS ) || ( status == FTPReply.DIRECTORY_STATUS ) || ( status
                == FTPReply.FILE_STATUS_OK ) // not in the RFC but used by some FTP servers
                || ( status == FTPReply.COMMAND_OK )     // not in the RFC but used by some FTP servers
                || ( status == FTPReply.SYSTEM_STATUS ) );
        }
        catch ( IOException e )
        {
            throw new TransferFailedException( "Error transferring file via FTP", e );
        }
        catch ( ResourceDoesNotExistException e )
        {
            return false;
        }
    }

    public boolean supportsDirectoryCopy()
    {
        return true;
    }

    public void putDirectory( File sourceDirectory, String destinationDirectory )
        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
    {

        // Change to root.
        try
        {
            if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
            {
                RepositoryPermissions permissions = getRepository().getPermissions();
                if ( !makeFtpDirectoryRecursive( getRepository().getBasedir(), permissions ) )
                {
                    throw new TransferFailedException(
                        "Required directory: '" + getRepository().getBasedir() + "' " + "could not get created" );
                }

                // try it again sam ...
                if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
                {
                    throw new TransferFailedException( "Required directory: '" + getRepository().getBasedir() + "' "
                                                           + "is missing and could not get created" );
                }
            }
        }
        catch ( IOException e )
        {
            throw new TransferFailedException( "Cannot change to root path " + getRepository().getBasedir(), e );
        }

        fireTransferDebug(
            "Recursively uploading directory " + sourceDirectory.getAbsolutePath() + " as " + destinationDirectory );
        ftpRecursivePut( sourceDirectory, destinationDirectory );
    }

    private void ftpRecursivePut( File sourceFile, String fileName )
        throws TransferFailedException
    {
        final RepositoryPermissions permissions = repository.getPermissions();

        fireTransferDebug( "processing = " + sourceFile.getAbsolutePath() + " as " + fileName );

        if ( sourceFile.isDirectory() )
        {
            if ( !fileName.equals( "." ) )
            {
                try
                {
                    // change directory if it already exists.
                    if ( !ftp.changeWorkingDirectory( fileName ) )
                    {
                        // first, try to create it
                        if ( makeFtpDirectoryRecursive( fileName, permissions ) )
                        {
                            if ( !ftp.changeWorkingDirectory( fileName ) )
                            {
                                throw new TransferFailedException(
                                    "Unable to change cwd on ftp server to " + fileName + " when processing "
                                        + sourceFile.getAbsolutePath() );
                            }
                        }
                        else
                        {
                            throw new TransferFailedException(
                                "Unable to create directory " + fileName + " when processing "
                                    + sourceFile.getAbsolutePath() );
                        }
                    }
                }
                catch ( IOException e )
                {
                    throw new TransferFailedException(
                        "IOException caught while processing path at " + sourceFile.getAbsolutePath(), e );
                }
            }

            File[] files = sourceFile.listFiles();
            if ( files != null && files.length > 0 )
            {
                fireTransferDebug( "listing children of = " + sourceFile.getAbsolutePath() + " found " + files.length );

                // Directories first, then files. Let's go deep early.
                for ( File file : files )
                {
                    if ( file.isDirectory() )
                    {
                        ftpRecursivePut( file, file.getName() );
                    }
                }
                for ( File file : files )
                {
                    if ( !file.isDirectory() )
                    {
                        ftpRecursivePut( file, file.getName() );
                    }
                }
            }

            // Step back up a directory once we're done with the contents of this one.
            try
            {
                ftp.changeToParentDirectory();
            }
            catch ( IOException e )
            {
                throw new TransferFailedException( "IOException caught while attempting to step up to parent directory"
                                                       + " after successfully processing "
                                                       + sourceFile.getAbsolutePath(), e );
            }
        }
        else
        {
            // Oh how I hope and pray, in denial, but today I am still just a file.

            FileInputStream sourceFileStream = null;
            try
            {
                sourceFileStream = new FileInputStream( sourceFile );

                // It's a file. Upload it in the current directory.
                if ( ftp.storeFile( fileName, sourceFileStream ) )
                {
                    if ( permissions != null )
                    {
                        // Process permissions; note that if we get errors or exceptions here, they are ignored.
                        // This appears to be a conscious decision, based on other parts of this code.
                        String group = permissions.getGroup();
                        if ( group != null )
                        {
                            try
                            {
                                ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() );
                            }
                            catch ( IOException e )
                            {
                                // ignore
                            }
                        }
                        String mode = permissions.getFileMode();
                        if ( mode != null )
                        {
                            try
                            {
                                ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() );
                            }
                            catch ( IOException e )
                            {
                                // ignore
                            }
                        }
                    }
                }
                else
                {
                    String msg =
                        "Cannot transfer resource:  '" + sourceFile.getAbsolutePath() + "' FTP Server response: "
                            + ftp.getReplyString();
                    throw new TransferFailedException( msg );
                }
            }
            catch ( IOException e )
            {
                throw new TransferFailedException(
                    "IOException caught while attempting to upload " + sourceFile.getAbsolutePath(), e );
            }
            finally
            {
                IOUtils.closeQuietly( sourceFileStream );
            }

        }

        fireTransferDebug( "completed = " + sourceFile.getAbsolutePath() );
    }

    /**
     * Set the permissions (if given) for the underlying folder.
     * Note: Since the FTP SITE command might be server dependent, we cannot
     * rely on the functionality available on each FTP server!
     * So we can only try and hope it works (and catch away all Exceptions).
     *
     * @param permissions group and directory permissions
     */
    private void setPermissions( RepositoryPermissions permissions )
    {
        if ( permissions != null )
        {
            // Process permissions; note that if we get errors or exceptions here, they are ignored.
            // This appears to be a conscious decision, based on other parts of this code.
            String group = permissions.getGroup();
            if ( group != null )
            {
                try
                {
                    ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() );
                }
                catch ( IOException e )
                {
                    // ignore
                }
            }
            String mode = permissions.getDirectoryMode();
            if ( mode != null )
            {
                try
                {
                    ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() );
                }
                catch ( IOException e )
                {
                    // ignore
                }
            }
        }
    }

    /**
     * Recursively create directories.
     *
     * @param fileName    the path to create (might be nested)
     * @param permissions
     * @return ok
     * @throws IOException
     */
    private boolean makeFtpDirectoryRecursive( String fileName, RepositoryPermissions permissions )
        throws IOException
    {
        if ( fileName == null || fileName.length() == 0
            || fileName.replace( '/', '_' ).trim().length() == 0 ) // if a string is '/', '//' or similar
        {
            return false;
        }

        int slashPos = fileName.indexOf( "/" );
        String oldPwd = null;
        boolean ok = true;

        if ( slashPos == 0 )
        {
            // this is an absolute directory
            oldPwd = ftp.printWorkingDirectory();

            // start with the root
            ftp.changeWorkingDirectory( "/" );
            fileName = fileName.substring( 1 );

            // look up the next path separator
            slashPos = fileName.indexOf( "/" );
        }

        if ( slashPos >= 0 && slashPos < ( fileName.length() - 1 ) ) // not only a slash at the end, like in 'newDir/'
        {
            if ( oldPwd == null )
            {
                oldPwd = ftp.printWorkingDirectory();
            }

            String nextDir = fileName.substring( 0, slashPos );

            // we only create the nextDir if it doesn't yet exist
            if ( !ftp.changeWorkingDirectory( nextDir ) )
            {
                ok &= ftp.makeDirectory( nextDir );
            }

            if ( ok )
            {
                // set the permissions for the freshly created directory
                setPermissions( permissions );

                ftp.changeWorkingDirectory( nextDir );

                // now create the deeper directories
                String remainingDirs = fileName.substring( slashPos + 1 );
                ok &= makeFtpDirectoryRecursive( remainingDirs, permissions );
            }
        }
        else
        {
            ok = ftp.makeDirectory( fileName );
        }

        if ( oldPwd != null )
        {
            // change back to the old working directory
            ftp.changeWorkingDirectory( oldPwd );
        }

        return ok;
    }

    public String getControlEncoding()
    {
        return controlEncoding;
    }

    public void setControlEncoding( String controlEncoding )
    {
        this.controlEncoding = controlEncoding;
    }
}
