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

/*
 * 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.sshd.common.util.DirectoryScanner;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.server.FileSystemAware;
import org.apache.sshd.server.FileSystemView;
import org.apache.sshd.server.SshFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;

/**
 * This commands provide SCP support on both server and client side.
 * Permissions and preservation of access / modification times on files
 * are not supported.
 * olamy : copy of a class from mina for changing return codes in case of file not found 1 warning instead of 2
 *
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public class ScpCommand
    implements Command, Runnable, FileSystemAware
{

    protected static final Logger LOG = LoggerFactory.getLogger( ScpCommand.class );

    protected static final int OK = 0;

    protected static final int WARNING = 1;

    protected static final int ERROR = 2;

    protected String name;

    protected boolean optR;

    protected boolean optT;

    protected boolean optF;

    protected boolean optV;

    protected boolean optD;

    protected boolean optP;

    protected FileSystemView root;

    protected String path;

    protected InputStream in;

    protected OutputStream out;

    protected OutputStream err;

    protected ExitCallback callback;

    protected IOException error;

    public ScpCommand( String[] args )
    {
        name = Arrays.asList( args ).toString();
        if ( LOG.isDebugEnabled() )
        {
            LOG.debug( "Executing command {}", name );
        }
        path = ".";
        for ( int i = 1; i < args.length; i++ )
        {
            if ( args[i].charAt( 0 ) == '-' )
            {
                for ( int j = 1; j < args[i].length(); j++ )
                {
                    switch ( args[i].charAt( j ) )
                    {
                        case 'f':
                            optF = true;
                            break;
                        case 'p':
                            optP = true;
                            break;
                        case 'r':
                            optR = true;
                            break;
                        case 't':
                            optT = true;
                            break;
                        case 'v':
                            optV = true;
                            break;
                        case 'd':
                            optD = true;
                            break;
                        default:
//                            error = new IOException("Unsupported option: " + args[i].charAt(j));
//                            return;
                    }
                }
            }
            else if ( i == args.length - 1 )
            {
                path = args[args.length - 1];
            }
        }
        if ( !optF && !optT )
        {
            error = new IOException( "Either -f or -t option should be set" );
        }
    }

    public void setInputStream( InputStream in )
    {
        this.in = in;
    }

    public void setOutputStream( OutputStream out )
    {
        this.out = out;
    }

    public void setErrorStream( OutputStream err )
    {
        this.err = err;
    }

    public void setExitCallback( ExitCallback callback )
    {
        this.callback = callback;
    }

    public void start( Environment env )
        throws IOException
    {
        if ( error != null )
        {
            throw error;
        }
        new Thread( this, "ScpCommand: " + name ).start();
    }

    public void destroy()
    {
    }

    public void run()
    {
        int exitValue = OK;
        String exitMessage = null;

        try
        {
            if ( optT )
            {
                ack();
                for (; ; )
                {
                    String line;
                    boolean isDir = false;
                    int c = readAck( true );
                    switch ( c )
                    {
                        case -1:
                            return;
                        case 'D':
                            isDir = true;
                        case 'C':
                            line = ( (char) c ) + readLine();
                            break;
                        case 'E':
                            readLine();
                            return;
                        default:
                            //a real ack that has been acted upon already
                            continue;
                    }

                    if ( optR && isDir )
                    {
                        writeDir( line, root.getFile( path ) );
                    }
                    else
                    {
                        writeFile( line, root.getFile( path ) );
                    }
                }
            }
            else if ( optF )
            {
                String pattern = path;
                int idx = pattern.indexOf( '*' );
                if ( idx >= 0 )
                {
                    String basedir = "";
                    int lastSep = pattern.substring( 0, idx ).lastIndexOf( '/' );
                    if ( lastSep >= 0 )
                    {
                        basedir = pattern.substring( 0, lastSep );
                        pattern = pattern.substring( lastSep + 1 );
                    }
                    String[] included = new DirectoryScanner( basedir, pattern ).scan();
                    for ( String path : included )
                    {
                        SshFile file = root.getFile( basedir + "/" + path );
                        if ( file.isFile() )
                        {
                            readFile( file );
                        }
                        else if ( file.isDirectory() )
                        {
                            if ( !optR )
                            {
                                out.write( WARNING );
                                out.write( ( path + " not a regular file\n" ).getBytes() );
                            }
                            else
                            {
                                readDir( file );
                            }
                        }
                        else
                        {
                            out.write( WARNING );
                            out.write( ( path + " unknown file type\n" ).getBytes() );
                        }
                    }
                }
                else
                {
                    String basedir = "";
                    int lastSep = pattern.lastIndexOf( '/' );
                    if ( lastSep >= 0 )
                    {
                        basedir = pattern.substring( 0, lastSep );
                        pattern = pattern.substring( lastSep + 1 );
                    }
                    SshFile file = root.getFile( basedir + "/" + pattern );
                    if ( !file.doesExist() )
                    {
                        exitValue = WARNING;
                        throw new IOException( file + ": no such file or directory" );
                    }
                    if ( file.isFile() )
                    {
                        readFile( file );
                    }
                    else if ( file.isDirectory() )
                    {
                        if ( !optR )
                        {
                            throw new IOException( file + " not a regular file" );
                        }
                        else
                        {
                            readDir( file );
                        }
                    }
                    else
                    {
                        throw new IOException( file + ": unknown file type" );
                    }
                }
            }
            else
            {
                throw new IOException( "Unsupported mode" );
            }
        }
        catch ( IOException e )
        {
            try
            {
                exitValue = ( exitValue != OK ? exitValue : ERROR );
                exitMessage = e.getMessage();
                out.write( exitValue );
                out.write( exitMessage.getBytes() );
                out.write( '\n' );
                out.flush();
            }
            catch ( IOException e2 )
            {
                // Ignore
            }
            LOG.info( "Error in scp command", e );
        }
        finally
        {
            if ( callback != null )
            {
                callback.onExit( exitValue, exitMessage );
            }
        }
    }

    protected void writeDir( String header, SshFile path )
        throws IOException
    {
        if ( LOG.isDebugEnabled() )
        {
            LOG.debug( "Writing dir {}", path );
        }
        if ( !header.startsWith( "D" ) )
        {
            throw new IOException( "Expected a D message but got '" + header + "'" );
        }

        String perms = header.substring( 1, 5 );
        int length = Integer.parseInt( header.substring( 6, header.indexOf( ' ', 6 ) ) );
        String name = header.substring( header.indexOf( ' ', 6 ) + 1 );

        if ( length != 0 )
        {
            throw new IOException( "Expected 0 length for directory but got " + length );
        }
        SshFile file;
        if ( path.doesExist() && path.isDirectory() )
        {
            file = root.getFile( path, name );
        }
        else if ( !path.doesExist() && path.getParentFile().doesExist() && path.getParentFile().isDirectory() )
        {
            file = path;
        }
        else
        {
            throw new IOException( "Can not write to " + path );
        }
        if ( !( file.doesExist() && file.isDirectory() ) && !file.mkdir() )
        {
            throw new IOException( "Could not create directory " + file );
        }

        ack();

        for (; ; )
        {
            header = readLine();
            if ( header.startsWith( "C" ) )
            {
                writeFile( header, file );
            }
            else if ( header.startsWith( "D" ) )
            {
                writeDir( header, file );
            }
            else if ( header.equals( "E" ) )
            {
                ack();
                break;
            }
            else
            {
                throw new IOException( "Unexpected message: '" + header + "'" );
            }
        }

    }

    protected void writeFile( String header, SshFile path )
        throws IOException
    {
        if ( LOG.isDebugEnabled() )
        {
            LOG.debug( "Writing file {}", path );
        }
        if ( !header.startsWith( "C" ) )
        {
            throw new IOException( "Expected a C message but got '" + header + "'" );
        }

        String perms = header.substring( 1, 5 );
        long length = Long.parseLong( header.substring( 6, header.indexOf( ' ', 6 ) ) );
        String name = header.substring( header.indexOf( ' ', 6 ) + 1 );

        SshFile file;
        if ( path.doesExist() && path.isDirectory() )
        {
            file = root.getFile( path, name );
        }
        else if ( path.doesExist() && path.isFile() )
        {
            file = path;
        }
        else if ( !path.doesExist() && path.getParentFile().doesExist() && path.getParentFile().isDirectory() )
        {
            file = path;
        }
        else
        {
            throw new IOException( "Can not write to " + path );
        }
        if ( file.doesExist() && file.isDirectory() )
        {
            throw new IOException( "File is a directory: " + file );
        }
        else if ( file.doesExist() && !file.isWritable() )
        {
            throw new IOException( "Can not write to file: " + file );
        }
        OutputStream os = file.createOutputStream( 0 );
        try
        {
            ack();

            byte[] buffer = new byte[8 * 1024];
            while ( length > 0 )
            {
                int len = (int) Math.min( length, buffer.length );
                len = in.read( buffer, 0, len );
                if ( len <= 0 )
                {
                    throw new IOException( "End of stream reached" );
                }
                os.write( buffer, 0, len );
                length -= len;
            }
        }
        finally
        {
            os.close();
        }

        ack();
        readAck( false );
    }

    protected String readLine()
        throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        for (; ; )
        {
            int c = in.read();
            if ( c == '\n' )
            {
                return baos.toString();
            }
            else if ( c == -1 )
            {
                throw new IOException( "End of stream" );
            }
            else
            {
                baos.write( c );
            }
        }
    }

    protected void readFile( SshFile path )
        throws IOException
    {
        if ( LOG.isDebugEnabled() )
        {
            LOG.debug( "Reading file {}", path );
        }
        StringBuilder buf = new StringBuilder();
        buf.append( "C" );
        buf.append( "0644" ); // what about perms
        buf.append( " " );
        buf.append( path.getSize() ); // length
        buf.append( " " );
        buf.append( path.getName() );
        buf.append( "\n" );
        out.write( buf.toString().getBytes() );
        out.flush();
        readAck( false );

        InputStream is = path.createInputStream( 0 );
        try
        {
            byte[] buffer = new byte[8 * 1024];
            for (; ; )
            {
                int len = is.read( buffer, 0, buffer.length );
                if ( len == -1 )
                {
                    break;
                }
                out.write( buffer, 0, len );
            }
        }
        finally
        {
            is.close();
        }
        ack();
        readAck( false );
    }

    protected void readDir( SshFile path )
        throws IOException
    {
        if ( LOG.isDebugEnabled() )
        {
            LOG.debug( "Reading directory {}", path );
        }
        StringBuilder buf = new StringBuilder();
        buf.append( "D" );
        buf.append( "0755" ); // what about perms
        buf.append( " " );
        buf.append( "0" ); // length
        buf.append( " " );
        buf.append( path.getName() );
        buf.append( "\n" );
        out.write( buf.toString().getBytes() );
        out.flush();
        readAck( false );

        for ( SshFile child : path.listSshFiles() )
        {
            if ( child.isFile() )
            {
                readFile( child );
            }
            else if ( child.isDirectory() )
            {
                readDir( child );
            }
        }

        out.write( "E\n".getBytes() );
        out.flush();
        readAck( false );
    }

    protected void ack()
        throws IOException
    {
        out.write( 0 );
        out.flush();
    }

    protected int readAck( boolean canEof )
        throws IOException
    {
        int c = in.read();
        switch ( c )
        {
            case -1:
                if ( !canEof )
                {
                    throw new EOFException();
                }
                break;
            case OK:
                break;
            case WARNING:
                LOG.warn( "Received warning: " + readLine() );
                break;
            case ERROR:
                throw new IOException( "Received nack: " + readLine() );
            default:
                break;
        }
        return c;
    }

    public void setFileSystemView( FileSystemView view )
    {
        this.root = view;
    }
}
