package com.limegroup.gnutella.browser;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

import com.limegroup.gnutella.ByteReader;
import com.limegroup.gnutella.Constants;
import com.limegroup.gnutella.MessageService;
import com.limegroup.gnutella.io.AcceptObserver;
import com.limegroup.gnutella.io.NIOServerSocket;
import com.limegroup.gnutella.io.SocketFactory;
import com.limegroup.gnutella.util.IOUtils;
import com.limegroup.gnutella.util.NetworkUtils;
import com.limegroup.gnutella.util.ThreadFactory;
import com.limegroup.gnutella.util.URLDecoder;

/**
 * Listens on an HTTP port, accepts incoming connections, and dispatches 
 * threads to handle requests.  This allows simple HTTP requests.
 */
public class HTTPAcceptor {
	/** Magnet request for a default action on parameters */
    private static final String MAGNET_DEFAULT = "magnet10/default.js?";
	/** Magnet request for a paused response */
    private static final String MAGNET_PAUSE   = "magnet10/pause";
	/** Start of Magnet URI */
    private static final String MAGNET         = "magnet:?";
	/** HTTP no content return */
	private static final String NOCONTENT      = "HTTP/1.1 204 No Content\r\n";
	/** Magnet detail command */
    private static final String MAGNETDETAIL   = "magcmd/detail?";

    /**
     * The socket that listens for incoming connections. Can be changed to
     * listen to new ports.
     *
     * LOCKING: obtain _socketLock before modifying either.  Notify _socketLock
     * when done.
     */
    private volatile ServerSocket _socket=null;
    private int 	 			  _port=45100;

	/** Try to supress duplicate requests from some browsers */
	private static String         _lastRequest     = null;
	private static long           _lastRequestTime = 0;


    /**
     * Starts listening to incoming connections.
     */
    public void start() {
        // Create the server socket, bind it to a port, and listen for
        // incoming connections.  If there are problems, we can continue
        // onward.
        //1. Try suggested port.
        try {
            setListeningPort(_port);
        } catch (IOException e) {
            boolean error = true;
            //2. Try 20 different consecutive ports from 45100 to 45119 (inclusive)
            //  no longer random, since this listening socket is used by the executable stub
            //  to launch magnet downloads, so the stub needs to know what (small) range of 
            //  ports to try...
            for (int i=1; i<20; i++) {
                _port = i+45100;
                try {
                    setListeningPort(_port);
                    error = false;
                    break;
                } catch (IOException ignored) {}
            }
            // no luck setting up? show user error message
            if(error) 
                MessageService.showError("ERROR_NO_PORTS_AVAILABLE");
        }
    }

    /**
     * @requires only one thread is calling this method at a time
     * @modifies this
     * @effects sets the port on which the ConnectionManager is listening.
     *  If that fails, this is <i>not</i> modified and IOException is thrown.
     *  If port==0, tells this to stop listening to incoming connections.
     *  This is properly synchronized and can be called even while run() is
     *  being called.
     */
    private void setListeningPort(int port) throws IOException {
        //1. Special case: if unchanged, do nothing.
        if (_socket!=null && _port==port) {
            return;
        }
        //2. Special case if port==0.  This ALWAYS works.
        //Note that we must close the socket BEFORE grabbing
        //the lock.  Otherwise deadlock will occur since
        //the acceptor thread is listening to the socket
        //while holding the lock.  Also note that port
        //will not have changed before we grab the lock.
        else if (port == 0) {
            IOUtils.close(_socket);
            _socket = null;
            _port = 0;
            return;
        }
        //3. Normal case.  See note about locking above.
        else {
            //a) Try new port.
            ServerSocket newSocket=null;
            try {
                newSocket = SocketFactory.newServerSocket(port, new SocketListener());
            } catch (IOException e) {
                throw e;
            }
            
            //b) Close old socket
            IOUtils.close(_socket);
            
            //c) Replace with new sock.
            _socket=newSocket;
            _port=port;
            return;
        }
    }

    /**
     *  Return the listening port.
     */
    public int getPort() {
		return _port;
	}
    
    /** Dispatches sockets to a thread that'll handle them. */
    private class SocketListener implements AcceptObserver {
        public void handleIOException(IOException ignored) {}
        public void shutdown() {}
        
        public void handleAccept(Socket client) {
            if(NetworkUtils.isLocalHost(client)) {                
                // Dispatch asynchronously.
                ThreadFactory.startThread(new ConnectionDispatchRunner(client), "ConnectionDispatchRunner");
            }
        }
    }
    
    private class ConnectionDispatchRunner implements Runnable {
        private Socket _socket;

        /**
         * @modifies socket, this' managers
         * @effects starts a new thread to handle the given socket and
         *  registers it with the appropriate protocol-specific manager.
         *  Returns once the thread has been started.  If socket does
         *  not speak a known protocol, closes the socket immediately and
         *  returns.
         */
        public ConnectionDispatchRunner(Socket socket) {
            _socket=socket;
        }

        public void run() {
            try {
                InputStream in = _socket.getInputStream(); 
                _socket.setSoTimeout(Constants.TIMEOUT);
                //dont read a word of size more than 8 
                //("GNUTELLA" is the longest word we know at this time)
                String word=IOUtils.readWord(in,8);
                _socket.setSoTimeout(0);
                
                if (word.equals("GET")) {
                    handleHTTPRequest(_socket);
                } else if (word.equals("MAGNET")) {
                    ExternalControl.fireMagnet(_socket);
                }
            } catch (IOException e) {
            } finally {
                IOUtils.close(_socket);
            }
        }
    }


	/**
	 * Read and parse any HTTP request.  Forward on to HTTPHandler.
	 *
	 * @param socket the <tt>Socket</tt> instance over which we're reading
	 */
	private void handleHTTPRequest(Socket socket) throws IOException {

		// Set the timeout so that we don't do block reading.
        socket.setSoTimeout(Constants.TIMEOUT);
		// open the stream from the socket for reading
		ByteReader br = new ByteReader(socket.getInputStream());
		
        // read the first line. if null, throw an exception
        String str = br.readLine();

		if (str == null) {
			throw new IOException();
		}
		str.trim();
		str = URLDecoder.decode(str);
		if (str.indexOf("magnet10") > 0) {
			int loc = 0;
			if ((loc = str.indexOf(MAGNET_DEFAULT)) > 0) {
				//Parse params out
				int loc2 = str.lastIndexOf(" HTTP");
				String command = 
				  str.substring(loc+MAGNET_DEFAULT.length(), loc2);
				triggerMagnetHandling(socket, MAGNET+command);
			} else if ((loc = str.indexOf(MAGNET_PAUSE)) > 0) {
				//System.out.println("Pause called:"+str);
		        try { Thread.sleep(250); } catch(Exception e) {};
				returnNoContent(socket);
			}
        } else if (str.indexOf(MAGNETDETAIL) >= 0) {
            int loc = 0;
            if ((loc = str.indexOf(MAGNETDETAIL)) < 0)
                return;
            int loc2 = str.lastIndexOf(" HTTP");
            String command = 
                  str.substring(loc+MAGNETDETAIL.length(), loc2);
            String page=MagnetHTML.buildMagnetDetailPage(command);
            
            HTTPHandler.createPage(socket, page);
		} else if (str.indexOf(MAGNET) >= 0) {
			// trigger an operation
			int loc  = str.indexOf(MAGNET);
			int loc2 = str.lastIndexOf(" HTTP");
			if ( loc < 0 )
				return;
			String command = str.substring(loc, loc2);
			triggerMagnetHandling(socket, command);
		} 
		try { socket.close(); } catch (IOException e) { }
  	}

	private void triggerMagnetHandling(Socket socket, String command) {

		// Supress duplicate requests from some browsers
		long curTime = (new Date()).getTime();
		if ( !(command.equals(_lastRequest) &&  
			   (curTime - _lastRequestTime) < 1500l) ) {
			
            // trigger an operation
            ExternalControl.handleMagnetRequest(command);
			_lastRequest     = command;
			_lastRequestTime = curTime;
		} 
        //else System.out.println("Duplicate request:"+command);
			
		returnNoContent(socket);
	}

    private void returnNoContent(Socket socket) {
        try {
            
            BufferedOutputStream out =
              new BufferedOutputStream(socket.getOutputStream());
            String s = NOCONTENT;
            byte[] bytes=s.getBytes();
            out.write(bytes);
            out.flush();
            
            socket.close();
        } catch (IOException e) {
        }
    }
}

