package com.runjva.sourceforge.jsocks.protocol;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;

/**
 * SocksServerSocket allows to accept connections from one particular host
 * through the SOCKS4 or SOCKS5 proxy.
 */
public class SocksServerSocket extends ServerSocket {
	// Data members
	protected SocksProxyBase proxy;
	protected String localHost;
	protected InetAddress localIP;
	protected int localPort;

	boolean doing_direct = false;
	InetAddress remoteAddr;

	/**
	 * Creates ServerSocket capable of accepting one connection through the
	 * firewall, uses default Proxy.
	 * 
	 * @param host
	 *            Host from which the connection should be recieved.
	 *@param port
	 *            Port number of the primary connection.
	 */
	public SocksServerSocket(String host, int port) throws SocksException,
			UnknownHostException, IOException {
		this(SocksProxyBase.defaultProxy, host, port);
	}

	/**
	 *Creates ServerSocket capable of accepting one connection through the
	 * firewall, uses given proxy.
	 * 
	 * @param p
	 *            Proxy object to use.
	 *@param host
	 *            Host from which the connection should be recieved.
	 *@param port
	 *            Port number of the primary connection.
	 */
	public SocksServerSocket(SocksProxyBase p, String host, int port)
			throws SocksException, UnknownHostException, IOException {

		super(0);
		if (p == null) {
			throw new SocksException(SocksProxyBase.SOCKS_NO_PROXY);
		}
		// proxy=p;
		proxy = p.copy();
		if (proxy.isDirect(host)) {
			remoteAddr = InetAddress.getByName(host);
			proxy = null;
			doDirect();
		} else {
			processReply(proxy.bind(host, port));
		}
	}

	/**
	 * Creates ServerSocket capable of accepting one connection through the
	 * firewall, uses default Proxy.
	 * 
	 * @param ip
	 *            Host from which the connection should be recieved.
	 *@param port
	 *            Port number of the primary connection.
	 */
	public SocksServerSocket(InetAddress ip, int port) throws SocksException,
			IOException {
		this(SocksProxyBase.defaultProxy, ip, port);
	}

	/**
	 *Creates ServerSocket capable of accepting one connection through the
	 * firewall, uses given proxy.
	 * 
	 * @param p
	 *            Proxy object to use.
	 *@param ip
	 *            Host from which the connection should be recieved.
	 *@param port
	 *            Port number of the primary connection.
	 */
	public SocksServerSocket(SocksProxyBase p, InetAddress ip, int port)
			throws SocksException, IOException {
		super(0);

		if (p == null) {
			throw new SocksException(SocksProxyBase.SOCKS_NO_PROXY);
		}
		this.proxy = p.copy();

		if (proxy.isDirect(ip)) {
			remoteAddr = ip;
			doDirect();
		} else {
			processReply(proxy.bind(ip, port));
		}
	}

	/**
	 * Accepts the incoming connection.
	 */
	public Socket accept() throws IOException {
		Socket s;

		if (!doing_direct) {
			if (proxy == null) {
				return null;
			}

			final ProxyMessage msg = proxy.accept();
			s = msg.ip == null ? new SocksSocket(msg.host, msg.port, proxy)
					: new SocksSocket(msg.ip, msg.port, proxy);
			// Set timeout back to 0
			proxy.proxySocket.setSoTimeout(0);
		} else { // Direct Connection

			// Mimic the proxy behaviour,
			// only accept connections from the speciefed host.
			while (true) {
				s = super.accept();
				if (s.getInetAddress().equals(remoteAddr)) {
					// got the connection from the right host
					// Close listenning socket.
					break;
				} else {
					s.close(); // Drop all connections from other hosts
				}
			}

		}
		proxy = null;
		// Return accepted socket
		return s;
	}

	/**
	 * Closes the connection to proxy if socket have not been accepted, if the
	 * direct connection is used, closes direct ServerSocket. If the client
	 * socket have been allready accepted, does nothing.
	 */
	public void close() throws IOException {
		super.close();
		if (proxy != null) {
			proxy.endSession();
		}
		proxy = null;
	}

	/**
	 * Get the name of the host proxy is using to listen for incoming
	 * connection.
	 * <P>
	 * Usefull when address is returned by proxy as the hostname.
	 * 
	 * @return the hostname of the address proxy is using to listen for incoming
	 *         connection.
	 */
	public String getHost() {
		return localHost;
	}

	/**
	 * Get address assigned by proxy to listen for incomming connections, or the
	 * local machine address if doing direct connection.
	 */
	public InetAddress getInetAddress() {
		if (localIP == null) {
			try {
				localIP = InetAddress.getByName(localHost);
			} catch (final UnknownHostException e) {
				return null;
			}
		}
		return localIP;
	}

	/**
	 * Get port assigned by proxy to listen for incoming connections, or the
	 * port chosen by local system, if accepting directly.
	 */
	public int getLocalPort() {
		return localPort;
	}

	/**
	 * Set Timeout.
	 * 
	 * @param timeout
	 *            Amount of time in milliseconds, accept should wait for
	 *            incoming connection before failing with exception. Zero
	 *            timeout implies infinity.
	 */
	public void setSoTimeout(int timeout) throws SocketException {
		super.setSoTimeout(timeout);
		if (!doing_direct) {
			proxy.proxySocket.setSoTimeout(timeout);
		}
	}

	// Private Methods
	// ////////////////

	private void processReply(ProxyMessage reply) throws SocksException {
		localPort = reply.port;
		/*
		 * If the server have assigned same host as it was contacted on it might
		 * return an address of all zeros
		 */
		if (reply.host.equals("0.0.0.0")) {
			localIP = proxy.proxyIP;
			localHost = localIP.getHostName();
		} else {
			localHost = reply.host;
			localIP = reply.ip;
		}
	}

	private void doDirect() {
		doing_direct = true;
		localPort = super.getLocalPort();
		localIP = super.getInetAddress();
		localHost = localIP.getHostName();
	}

}
