/*
 * Copyright 1999-2006 University of Chicago
 *
 * 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.
 */
package org.globus.ftp.extended;

import java.net.Socket;
import java.net.UnknownHostException;
import java.io.IOException;
import java.io.DataOutputStream;

import org.globus.util.Util;
import org.globus.net.ServerSocketFactory;
import org.globus.gsi.gssapi.net.GssSocket;
import org.globus.gsi.gssapi.net.GssSocketFactory;
import org.globus.gsi.gssapi.auth.SelfAuthorization;
import org.globus.gsi.gssapi.auth.IdentityAuthorization;
import org.globus.gsi.gssapi.GSSConstants;
import org.globus.gsi.GSIConstants;
import org.globus.ftp.GridFTPSession;
import org.globus.ftp.HostPort;
import org.globus.ftp.HostPort6;
import org.globus.ftp.HostPortList;
import org.globus.ftp.RetrieveOptions;
import org.globus.ftp.DataSink;
import org.globus.ftp.DataSource;
import org.globus.ftp.DataChannelAuthentication;
import org.globus.ftp.Options;
import org.globus.ftp.Session;
import org.globus.ftp.dc.EBlockParallelTransferContext;
import org.globus.ftp.dc.ManagedSocketBox;
import org.globus.ftp.dc.SocketBox;
import org.globus.ftp.dc.SocketOperator;
import org.globus.ftp.dc.SocketPool;
import org.globus.ftp.dc.TransferContext;
import org.globus.ftp.dc.StripeContextManager;
import org.globus.ftp.dc.EBlockImageDCWriter;
import org.globus.ftp.dc.TransferThreadManager;
import org.globus.ftp.exception.DataChannelException;
import org.globus.ftp.exception.ClientException;
import org.globus.ftp.vanilla.FTPServerFacade;

import org.gridforum.jgss.ExtendedGSSManager;
import org.gridforum.jgss.ExtendedGSSContext;

import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSManager;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class GridFTPServerFacade extends FTPServerFacade {

    private static Log logger =
        LogFactory.getLog(GridFTPServerFacade.class.getName());

    // utility alias to session
    protected GridFTPSession gSession = null;
    protected SocketPool socketPool = null;
    protected TransferThreadManager transferThreadManager = null;

    // current transfer, if striped retrieve, is associated to this manager
    // (striped store does not suffer for EOD complications and don't need
    // manager)
    protected StripeContextManager stripeRetrContextManager = null;

    public GridFTPServerFacade(GridFTPControlChannel remoteControlChannel) {
        super(remoteControlChannel);
        gSession = new GridFTPSession();
        session = gSession;
        // make sure this doesn't get used
        dataChannelFactory = null;
        socketPool = new SocketPool();
        transferThreadManager =  createTransferThreadManager();
    }

    public void setCredential(GSSCredential cred) {
        gSession.credential = cred;
    }

    public void setDataChannelProtection(int protection) {
        gSession.dataChannelProtection = protection;
    }

    public void setDataChannelAuthentication(DataChannelAuthentication authentication) {
        gSession.dataChannelAuthentication = authentication;
    }

    public void setOptions(Options opts) {
        if (opts instanceof RetrieveOptions) {
            gSession.parallel =
                ((RetrieveOptions) opts).getStartingParallelism();
            logger.debug("parallelism set to " + gSession.parallel);
        }
    }

    /**
       This method needs to be called BEFORE the local socket(s) get created.
       In other words, before setActive(), setPassive(), get(), put(), etc.
    **/
    public void setTCPBufferSize(final int size) throws ClientException {
        logger.debug("Changing local TCP buffer setting to " + size);

        gSession.TCPBufferSize = size;

        SocketOperator op = new SocketOperator() {
                public void operate(SocketBox s) throws Exception {

                    // synchronize to prevent race condition against
                    // the socket initialization code that also sets
                    // TCP buffer (GridFTPActiveConnectTask)
                    synchronized (s) {
                        logger.debug(
                                     "Changing local socket's TCP buffer to " + size);
                        Socket mySocket = s.getSocket();
                        if (mySocket != null) {
                            mySocket.setReceiveBufferSize(size);
                            mySocket.setSendBufferSize(size);
                        } else {
                            logger.debug(
                                         "the socket is null. probably being initialized");
                        }
                    }
                }
        };
        try {
            socketPool.applyToAll(op);
        } catch (Exception e) {
            ClientException ce =
                new ClientException(ClientException.SOCKET_OP_FAILED);
            ce.setRootCause(e);
            throw ce;
        }
    }

    protected void transferAbort() {
        if (session.serverMode == Session.SERVER_PASSIVE) {
            unblockServer();
            transferThreadManager.stopTaskThread();
        }
    }

    /**
       All sockets opened when this server was active
       should send a special EBlock header before closing.
    */
    private void closeOutgoingSockets() throws ClientException {

        SocketOperator op = new SocketOperator() {
                public void operate(SocketBox sb) throws Exception {
                    if (((ManagedSocketBox) sb).isReusable()) {
                        Socket s = sb.getSocket();
                        if (s != null) {
                            // write the closing Eblock and close the socket
                            EBlockImageDCWriter.close(
                                  new DataOutputStream(s.getOutputStream()));
                        }
                    }
                }
       };

        try {
            socketPool.applyToAll(op);
        } catch (IOException e) {
            // ignore - sometimes server might close the socket
        } catch (Exception e) {
            ClientException ce =
                new ClientException(ClientException.SOCKET_OP_FAILED);
            ce.setRootCause(e);
            throw ce;
        }
    }

    public void setActive(HostPort hp)
        throws UnknownHostException, ClientException, IOException {
        if (logger.isDebugEnabled()) {
            logger.debug("hostport: " + hp.getHost() + " " + hp.getPort());
        }

        if (session.serverMode == Session.SERVER_ACTIVE) {
            closeOutgoingSockets();
        }

        socketPool.flush();

        session.serverMode = Session.SERVER_ACTIVE;
        // may be needed later, if parallelism increases and
        // new connections need to be open
        this.remoteServerAddress = hp;

        transferThreadManager.activeConnect(hp, gSession.parallel);
    }

    public void setStripedActive(HostPortList hpl)
        throws UnknownHostException, IOException {
        if (hpl == null) {
            throw new IllegalArgumentException("null HostPortList");
        }
        int stripes = hpl.size();
        if (stripes < 1) {
            throw new IllegalArgumentException("empty HostPortList");
        }

        socketPool.flush(); // = new SocketBox[pathes * stripes];

        //create context manager that will be used by retrieve()
        this.stripeRetrContextManager =
            new StripeContextManager(stripes, socketPool, this);

        int pathes = gSession.parallel;
        gSession.serverMode = GridFTPSession.SERVER_EACT;

        for (int stripe = 0; stripe < stripes; stripe++) {
            transferThreadManager.activeConnect(hpl.get(stripe), pathes);

        }
    }

    public HostPort setPassive(int port, int queue) throws IOException {
        // remove existing sockets, if any
        socketPool.flush();

        return super.setPassive(port, queue);
    }

    public HostPortList setStripedPassive() throws IOException {
        return setStripedPassive(ANY_PORT, DEFAULT_QUEUE);
    }

    public HostPortList setStripedPassive(int port, int queue)
        throws IOException {

        // remove existing sockets, if any
        socketPool.flush();

        if (serverSocket == null) {
            ServerSocketFactory factory =
                ServerSocketFactory.getDefault();
            serverSocket = factory.createServerSocket(port, queue);
        }

        gSession.serverMode = GridFTPSession.SERVER_EPAS;
        gSession.serverAddressList = new HostPortList();

        String address = Util.getLocalHostAddress();
        int localPort = serverSocket.getLocalPort();

        HostPort hp = null;
        if (remoteControlChannel.isIPv6()) {
            String version = HostPort6.getIPAddressVersion(address);
            hp = new HostPort6(version, address, localPort);
        } else {
            hp = new HostPort(address, localPort);
        }

        gSession.serverAddressList.add(hp);

        logger.debug("started single striped passive server at port " +
                     ((HostPort) gSession.serverAddressList.get(0)).getPort());

        return gSession.serverAddressList;
    }

    /**
       Store the data from the data channel to the data sink.
       Does not block.
       If operation fails, exception might be thrown via local control channel.
       @param sink source of data
    **/
    public void store(DataSink sink) {
        try {

            localControlChannel.resetReplyCount();

            if (session.transferMode != GridFTPSession.MODE_EBLOCK) {
                //
                // no EBLOCK
                //
                EBlockParallelTransferContext context =
                    (EBlockParallelTransferContext) createTransferContext();
                context.setEodsTotal(0);

                if (session.serverMode == Session.SERVER_PASSIVE) {
                    transferThreadManager.passiveConnect(
                                                         sink,
                                                         context,
                                                         1,
                                                         serverSocket);

                } else {
                    //1 non reusable connection
                    transferThreadManager.startTransfer(
                                                        sink,
                                                        context,
                                                        1,
                                                        ManagedSocketBox.NON_REUSABLE);
                }

            } else if (
                       session.serverMode != GridFTPSession.SERVER_EPAS
                       && session.serverMode != GridFTPSession.SERVER_PASSIVE) {
                //
                // EBLOCK, local server not passive
                //
                exceptionToControlChannel(
                                          new DataChannelException(
                                                                   DataChannelException.BAD_SERVER_MODE),
                                          "refusing to store with active mode");
            } else {
                //
                // EBLOCK, local server passive
                //

                // data channels will
                // share this transfer context
                EBlockParallelTransferContext context =
                    (EBlockParallelTransferContext) createTransferContext();

                // we are the passive side, so we don't really get to decide
                // how many connections will be used
                int willReuseConnections = socketPool.countFree();
                int needNewConnections = 0;
                if (gSession.parallel > willReuseConnections) {
                    needNewConnections = gSession.parallel - willReuseConnections;
                }

                logger.debug("will reuse " + willReuseConnections +
                             " connections and start " + needNewConnections +
                             " new ones.");

                transferThreadManager.startTransfer(
                                                    sink,
                                                    context,
                                                    willReuseConnections,
                                                    ManagedSocketBox.REUSABLE);

                if (needNewConnections > 0) {
                    transferThreadManager.passiveConnect(
                                                         sink,
                                                         context,
                                                         needNewConnections,
                                                         serverSocket);
                }
            }
        } catch (Exception e) {
            exceptionToControlChannel(e, "ocurred during store()");
        }
    }

    /**
       Retrieve the data from the data source and write to the data channel.
       This method does not block.
       If operation fails, exception might be thrown via local control channel.
       @param source source of data
    **/
    public void retrieve(DataSource source) {
        try {
            localControlChannel.resetReplyCount();

            if (session.transferMode != GridFTPSession.MODE_EBLOCK) {

                //
                // No EBLOCK
                //

                EBlockParallelTransferContext context =
                    (EBlockParallelTransferContext) createTransferContext();
                context.setEodsTotal(0);

                logger.debug("starting outgoing transfer without mode E");
                if (session.serverMode == Session.SERVER_PASSIVE) {

                    transferThreadManager.passiveConnect(source, context, serverSocket);

                } else {

                    transferThreadManager.startTransfer(
                                                        source,
                                                        context,
                                                        1,
                                                        ManagedSocketBox.NON_REUSABLE);
                }

                return;

            } else if (session.serverMode == Session.SERVER_ACTIVE) {

                //
                // EBLOCK, no striping
                //

                // data channels will share this transfer context
                EBlockParallelTransferContext context =
                    (EBlockParallelTransferContext) createTransferContext();

                int total = gSession.parallel;
                //we should send as many EODS as there are parallel streams
                context.setEodsTotal(total);

                int free = socketPool.countFree();
                int willReuseConnections = (total > free) ? free : total;
                int willCloseConnections = (free > total) ? free - total : 0;
                int needNewConnections = (total > free) ? total - free: 0;

                logger.debug("will reuse " + willReuseConnections +
                             " connections, start " + needNewConnections +
                             " new ones, and close " + willCloseConnections);

                if (needNewConnections > 0 ) {
                    transferThreadManager.activeConnect(this.remoteServerAddress,
                                                        needNewConnections);
                }

                if (willCloseConnections > 0) {
                    transferThreadManager.activeClose(context,
                                                      willCloseConnections);
                }

                transferThreadManager.startTransfer(
                                                    source,
                                                    context,
                                                    willReuseConnections + needNewConnections,
                                                    ManagedSocketBox.REUSABLE);

            } else if (session.serverMode == GridFTPSession.SERVER_EACT) {

                //
                // EBLOCK, striping
                //

                if (stripeRetrContextManager == null) {
                    throw new IllegalStateException();
                }

                int stripes = stripeRetrContextManager.getStripes();

                for (int stripe = 0; stripe < stripes; stripe++) {

                    EBlockParallelTransferContext context =
                        stripeRetrContextManager.getStripeContext(stripe);
                    context.setEodsTotal(gSession.parallel);

                    transferThreadManager.startTransfer(
                                                        source,
                                                        context,
                                                        gSession.parallel,
                                                        ManagedSocketBox.REUSABLE);

                }

            } else {

                //
                // EBLOCK and local server not active
                //

                throw new DataChannelException(
                                               DataChannelException.BAD_SERVER_MODE);
            }
        } catch (Exception e) {
            exceptionToControlChannel(e, "ocurred during retrieve()");
        }
    };

    //override
    public void abort() throws IOException {
        super.abort();
        if (socketPool != null) {
            socketPool.flush();
        }
    }

    //override
    public void close() throws IOException {
        super.close();
        if (transferThreadManager != null) {
            transferThreadManager.close();
        }
    }

    /**
        authenticate socket.
        if protection on, return authenticated socket wrapped over the original simpleSocket,
        else return original socket.
    **/
    public static Socket authenticate(
                                      Socket simpleSocket,
                                      boolean isClientSocket,
                                      GSSCredential credential,
                                      int protection,
                                      DataChannelAuthentication dcau)
        throws Exception {

        GSSContext gssContext = null;
        GSSManager manager = ExtendedGSSManager.getInstance();

        if (isClientSocket) {
            gssContext =
                manager.createContext(
                                      null,
                                      GSSConstants.MECH_OID,
                                      credential,
                                      GSSContext.DEFAULT_LIFETIME);
        } else {
            gssContext = manager.createContext(credential);
        }

        if (protection != GridFTPSession.PROTECTION_CLEAR) {
            ((ExtendedGSSContext) gssContext).setOption(
                                                        GSSConstants.GSS_MODE,
                                                        GSIConstants.MODE_SSL);
        }

        gssContext.requestConf(protection == GridFTPSession.PROTECTION_PRIVATE);

        //Wrap the simple socket with GSI
        logger.debug("Creating secure socket");

        GssSocketFactory factory = GssSocketFactory.getDefault();
        GssSocket secureSocket =
            (GssSocket) factory.createSocket(simpleSocket, null, 0, gssContext);

        secureSocket.setUseClientMode(isClientSocket);

        if (dcau == null) {
            secureSocket.setAuthorization(null);
        } else if (dcau == DataChannelAuthentication.SELF) {
            secureSocket.setAuthorization(SelfAuthorization.getInstance());
        } else if (dcau == DataChannelAuthentication.NONE) {
            // this should never be
        } else if (dcau instanceof DataChannelAuthentication) {
            // dcau.toFtpCmdArgument() kinda hackish but it works
            secureSocket.setAuthorization(
                                          new IdentityAuthorization(dcau.toFtpCmdArgument()));
        }

        /* that will force handshake */
        secureSocket.getOutputStream().flush();

        if (protection == GridFTPSession.PROTECTION_SAFE ||
            protection == GridFTPSession.PROTECTION_PRIVATE) {
            logger.debug("Data channel protection: on");
            return secureSocket;
        } else { // PROTECTION_CLEAR
            logger.debug("Data channel protection: off");
            return simpleSocket;
        }
    }

    protected TransferContext createTransferContext() {
        EBlockParallelTransferContext context =
            new EBlockParallelTransferContext();
        context.setSocketPool(socketPool);
        context.setTransferThreadManager(this.transferThreadManager);
        return context;
    }

    public TransferThreadManager createTransferThreadManager() {
        return new TransferThreadManager(socketPool,
                                         this,
                                         localControlChannel,
                                         gSession);
    }

}
