package com.limegroup.gnutella;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;

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

import com.limegroup.gnutella.http.HTTPRequestMethod;
import com.limegroup.gnutella.io.ConnectObserver;
import com.limegroup.gnutella.statistics.UploadStat;
import com.limegroup.gnutella.udpconnect.UDPConnection;
import com.limegroup.gnutella.util.IOUtils;
import com.limegroup.gnutella.util.ManagedThread;
import com.limegroup.gnutella.util.NetworkUtils;
import com.limegroup.gnutella.util.Sockets;
import com.limegroup.gnutella.util.ThreadFactory;

/**
 * Manages state for push upload requests.
 */
public final class PushManager {
    
    private static final Log LOG = LogFactory.getLog(PushManager.class);

    /**
     * The timeout for the connect time while establishing the socket. Set to
     * the same value as NORMAL_CONNECT_TIME is ManagedDownloader.
     */
    private static final int CONNECT_TIMEOUT = 10000;//10 secs


	/**
	 * Accepts a new push upload.
     * NON-BLOCKING: creates a new thread to transfer the file.
	 * <p>
     * The thread connects to the other side, waits for a GET/HEAD,
     * and delegates to the UploaderManager.acceptUpload method with the
     * socket it created.
     * Essentially, this is a reverse-Acceptor.
     * <p>
     * No file and index are needed since the GET/HEAD will include that
     * information.  Just put in our first file and filename to create a
     * well-formed.
	 * @param host the ip address of the host to upload to
	 * @param port the port over which the transfer will occur
	 * @param guid the unique identifying client guid of the uploading client
     * @param forceAllow whether or not to force the UploadManager to send
     *  accept this request when it comes back.
     * @param isFWTransfer whether or not to use a UDP pipe to service this
     * upload.
	 */
	public void acceptPushUpload(final String host, 
                                 final int port, 
                                 final String guid,
                                 final boolean forceAllow,
                                 final boolean isFWTransfer) {
        if (LOG.isDebugEnabled())
            LOG.debug("Accepting Push Upload from ip:" + host + " port:" + port + " FW:" + isFWTransfer);
                                    
        if( host == null )
            throw new NullPointerException("null host");
        if( !NetworkUtils.isValidPort(port) )
            throw new IllegalArgumentException("invalid port: " + port);
        if( guid == null )
            throw new NullPointerException("null guid");
                                    

        FileManager fm = RouterService.getFileManager();
        
        // TODO: why is this check here?  it's a tiny optimization,
        // but could potentially kill any sharing of files that aren't
        // counted in the library.
        if (fm.getNumFiles() < 1 && fm.getNumIncompleteFiles() < 1)
            return;

        // We used to have code here that tested if the guy we are pushing to is
        // 1) hammering us, or 2) is actually firewalled.  1) is done above us
        // now, and 2) isn't as much an issue with the advent of connectback
        
        PushData data = new PushData(host, port, guid, forceAllow);
        
        // If the transfer is to be done using FW-FW, then immediately start a new thread
        // which will connect using FWT.  Otherwise, do a non-blocking connect and have
        // the observer spawn the thread only if it succesfully connected.
        if(isFWTransfer) {
            if(LOG.isDebugEnabled())
                LOG.debug("Adding push observer FW-FW to host: " + host + ":" + port);
            UDPConnection socket = new UDPConnection();
            socket.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT*2, new PushObserver(data, true));
        } else {
            if (LOG.isDebugEnabled())
                LOG.debug("Adding push observer to host: " + host + ":" + port);
            try {
                Sockets.connect(host, port, CONNECT_TIMEOUT, new PushObserver(data, false));
            } catch(IOException iox) {
                UploadStat.PUSH_FAILED.incrementStat();
            }
        }
    }
    
    /** A simple collection of Push information */
    private static class PushData {
        private final String host;
        private final int port;
        private final String guid;
        private final boolean forceAllow;
        
        PushData(String host, int port, String guid, boolean forceAllow) {
            this.host = host;
            this.port = port;
            this.guid = guid;
            this.forceAllow = forceAllow;
        }
        
        public boolean isForceAllow() {
            return forceAllow;
        }
        public String getGuid() {
            return guid;
        }
        public String getHost() {
            return host;
        }
        public int getPort() {
            return port;
        }
    }
    
    /** Non-blocking observer for connect events related to pushing. */
    private static class PushObserver implements ConnectObserver {
        private final PushData data;
        private final boolean fwt;
        
        PushObserver(PushData data, boolean fwt) {
            this.data = data;
            this.fwt = fwt;
        }        
        
        public void handleIOException(IOException iox) {}

        /** Increments the PUSH_FAILED stat and does nothing else. */
        public void shutdown() {
            if(LOG.isDebugEnabled())
                LOG.debug("Push (fwt: " + fwt + ") connect to: " + data.getHost() + ":" + data.getPort() + " failed");
            if(fwt)
                UploadStat.FW_FW_FAILURE.incrementStat();
            else
                UploadStat.PUSH_FAILED.incrementStat();
        }

        /** Starts a new thread that'll do the pushing. */
        public void handleConnect(Socket socket) throws IOException {
            if(LOG.isDebugEnabled())
                LOG.debug("Push (fwt: " + fwt + ") connect to: " + data.getHost() + ":" + data.getPort() + " succeeded");
            ThreadFactory.startThread(new Pusher(data, socket, fwt), "PushUploadThread");
        }
    }    

    /** A runnable that starts a push transfer. */
    private static class Pusher implements Runnable {
        PushData data;
        private Socket socket;
        private boolean fwTransfer;
        
        Pusher(PushData data, Socket socket, boolean fwt) {
            this.data = data;
            this.socket = socket;
            this.fwTransfer = fwt;
        }

        public void run() {
            try {
                OutputStream ostream = socket.getOutputStream();
                String giv = "GIV 0:" + data.getGuid() + "/file\n\n";
                ostream.write(giv.getBytes());
                ostream.flush();

                // try to read a GET or HEAD for only 30 seconds.
                socket.setSoTimeout(30 * 1000);

                // read GET or HEAD and delegate appropriately.
                String word = IOUtils.readWord(socket.getInputStream(), 4);
                if (fwTransfer)
                    UploadStat.FW_FW_SUCCESS.incrementStat();

                if (word.equals("GET")) {
                    UploadStat.PUSHED_GET.incrementStat();
                    RouterService.getUploadManager().acceptUpload(HTTPRequestMethod.GET, socket, data.isForceAllow());
                } else if (word.equals("HEAD")) {
                    UploadStat.PUSHED_HEAD.incrementStat();
                    RouterService.getUploadManager().acceptUpload(HTTPRequestMethod.HEAD, socket, data.isForceAllow());
                } else {
                    UploadStat.PUSHED_UNKNOWN.incrementStat();
                    throw new IOException();
                }
            } catch (IOException ioe) {
                if(LOG.isDebugEnabled())
                    LOG.debug("Failed push connect/transfer to " + data.getHost() + ":" + data.getPort() + ", fwt: " + fwTransfer);
                if (fwTransfer)
                    UploadStat.FW_FW_FAILURE.incrementStat();
                UploadStat.PUSH_FAILED.incrementStat();
            } finally {
                IOUtils.close(socket);
            }
        }
    }
}
