/*
 * @(#)TimedSocket.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */

package com.jscape.inet.util;


import com.jscape.util.IOTools;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;


/**
 * Implements methods for creating sockets within a defined timeout.
 * <p/>
 * Timeout is based upon time to establish connection, as well as reading
 * blocking I/O data from the incoming socket connection.<br>
 * <br>Example Usage:<p>
 * <pre>
 * String hostname = "www.myserver.com";
 * int port = 80;
 * int timeout = 5000; // 5 seconds
 * try
 * {
 *   Socket con = TimedSocket.getSocket(hostname,port,timeout);
 * }
 * catch(Exception ioe)
 * {
 *   // handle exception
 * }
 * </pre>
 */
@SuppressWarnings("unchecked")
public class TimedSocket {

    /**
     * Creates a new <code>Socket</code> instance.
     *
     * @param addr      the <code>InetAddress</code> to connect to
     * @param port      the port to connect to
     * @param localAddr the local <code>InetAddress</code> to connect from
     * @param localPort the local port to connect from
     * @param delay     timeout in milliseconds
     *
     * @return a <code>Socket</code>
     *
     * @throws java.io.IOException timeout is reached or other I/O error occurs
     */
    public static Socket getSocket(InetAddress addr,
                                   int port,
                                   InetAddress localAddr,
                                   int localPort,
                                   long delay)
            throws IOException {
        Socket sock = null;
        String version = System.getProperty("java.version");
        if (versionWithThreads(version)) {
            SocketThread socketThread = new SocketThread(addr, port, localAddr, localPort, delay);
            sock = waitForANewSocket(delay, socketThread);
            socketThread = null;
        } else {

            sock = createSpecialSocket(addr, port, delay, 0, 0, localAddr, localPort);


        }
        return sock;
    }

    /**
     * Gets if java version is minor than 1.4.
     *
     * @param version Current Java version
     *
     * @return <code>true</code> if java version is 1.2 or 1.3
     */
    private static boolean versionWithThreads(String version) {
        boolean result = false;

        if (version.startsWith("1.2") || version.startsWith("1.3") || version.startsWith("1.0") || version.startsWith("1.1")) {
            result = true;
        }
        return result;
    }

    /**
     * Creates a new <code>Socket</code> instance.
     *
     * @param addr          the <code>InetAddress</code> to connect to
     * @param port          the port to connect to
     * @param localAddr     the local <code>InetAddress</code> to connect from
     * @param localPort     the local port to connect from
     * @param delay         timeout in milliseconds
     * @param sendBuffer    Send socket buffer value.
     * @param receiveBuffer Receive socket buffer value
     *
     * @return a <code>Socket</code>
     *
     * @throws java.io.IOException timeout is reached or other I/O error occurs
     */
    public static Socket getSocket(InetAddress addr,
                                   int port,
                                   InetAddress localAddr,
                                   int localPort,
                                   long delay,
                                   int sendBuffer,
                                   int receiveBuffer)
            throws IOException {
        Socket sock = null;
        String version = System.getProperty("java.version");
        if (versionWithThreads(version)) {

            SocketThread socketThread = new SocketThread(addr, port, localAddr,
                                                         localPort, delay, sendBuffer, receiveBuffer);

            sock = waitForANewSocket(delay, socketThread);

            socketThread = null;
        } else {
            sock = createSpecialSocket(addr, port, delay, sendBuffer, receiveBuffer, localAddr, localPort);
        }
        return sock;
    }

    /**
     * Creates a new <code>Socket</code> instance.
     *
     * @param addr  the <code>InetAddress</code> to connect to
     * @param port  the port to connect to
     * @param delay timeout in milliseconds
     *
     * @return a <code>Socket</code>
     *
     * @throws java.io.IOException timeout is reached or other I/O error occurs
     */
    public static Socket getSocket(InetAddress addr, int port, long delay)
            throws IOException {
        Socket sock = null;
        String version = System.getProperty("java.version");
        if (versionWithThreads(version)) {
            SocketThread socketThread = new SocketThread(addr, port, delay);
            sock = waitForANewSocket(delay, socketThread);
            socketThread = null;
        } else {
            sock = createSpecialSocket(addr, port, delay, 0, 0, null, 0);
        }
        return sock;
    }

    /**
     * Waits for a socket connection.
     *
     * @param timeout      Time will wait
     * @param socketThread Thread used to obtain the socket connection
     *
     * @return New socket connection
     *
     * @throws IOException If an error occurs.
     */
    private static Socket waitForANewSocket(long timeout, SocketThread socketThread)
            throws IOException {

        long delay = timeout;

        SleepThread sleepThread = new SleepThread(false, delay);
        socketThread.setSleepThread(sleepThread);
        socketThread.start();
        sleepThread.start();

        Socket sock = null;

        while (!sleepThread.getStop()) {
            // ok
        }

        if (socketThread.isConnected()) {
            // Yes ... assign to sock variable, and break out of loop
            sock = socketThread.getSocket();
        } else {
            // Check to see if an error occurred
            if (socketThread.isError()) {
                // No connection could be established
                IOException e = socketThread.getException();
                socketThread.setRunning(false);
                socketThread = null;
                throw (e);
            } else {
                socketThread.setRunning(false);
                socketThread = null;
                throw new InterruptedIOException(
                        "Could not connect for " + delay + " milliseconds");
            }
        }
        sleepThread = null;
        return sock;

    }

    /**
     * Creates a new <code>Socket</code> instance.
     *
     * @param addr  the <code>InetAddress</code> to connect to
     * @param port  the port to connect to
     * @param delay timeout in milliseconds
     *
     * @return a <code>Socket</code>
     *
     * @throws java.io.IOException timeout is reached or other I/O error occurs
     */
    public static Socket getSocket(InetAddress addr,
                                   int port,
                                   long delay,
                                   int sendBuffer,
                                   int receiveBuffer)
            throws IOException {
        Socket sock = null;
        String version = System.getProperty("java.version");
        if (versionWithThreads(version)) {
            SocketThread socketThread = new SocketThread(addr, port, delay, sendBuffer, receiveBuffer);

            sock = waitForANewSocket(delay, socketThread);

            socketThread = null;
        } else {
            sock = createSpecialSocket(addr, port, delay, sendBuffer, receiveBuffer, null, 0);
        }
        return sock;
    }

    /**
     * Creates a new <code>Socket</code> instance.
     *
     * @param host          the host to connect to
     * @param port          the port to connect to
     * @param delay         timeout in milliseconds
     * @param proxyHost     the proxy host to connect through
     * @param proxyPort     the proxy port to connect on
     * @param proxyUsername The username to use
     * @param proxyPassword The proxy password to use
     * @param proxyType     either HTTP or SOCKS5
     *
     * @return a new <code>Socket</code> instance
     *
     * @throws java.io.IOException timeout is reached or other I/O error occurs
     */
    public static Socket getSocket(String host,
                                   int port,
                                   long delay,
                                   String proxyHost,
                                   int proxyPort,
                                   String proxyUsername,
                                   String proxyPassword,
                                   String proxyType)
            throws IOException {
        Socket sock = null;
        String version = System.getProperty("java.version");
        if (versionWithThreads(version)) {
            //  support DNS name resolution through proxy
            SocketThread socketThread = null;
            try {
                InetAddress addr = InetAddress.getByName(host);
                socketThread = new SocketThread(addr, port, delay, proxyHost, proxyPort,
                                                proxyUsername, proxyPassword, proxyType);
            } catch (Exception e) {
                socketThread = new SocketThread(host, port, delay, proxyHost, proxyPort,
                                                proxyUsername, proxyPassword, proxyType);
            }
            sock = waitForANewSocket(delay, socketThread);

            socketThread = null;
        } else {
            InetAddress addr = InetAddress.getByName(host);
            SocketThread socketCreator = new SocketThread(addr, port, delay, proxyHost, proxyPort,
                                                          proxyUsername, proxyPassword, proxyType);
            try {
                sock = socketCreator.getSocketUsingProxy();

                if (sock == null) {
                    throw new Exception("Connection not established");
                }

            } catch (Exception e) {
                throw new IOException(e.getMessage());
            }
        }
        return sock;
    }

    /**
     * Creates a new <code>Socket</code> instance.
     *
     * @param host  the host to connect to
     * @param port  the port to connect to
     * @param delay timeout in milliseconds
     *
     * @return a <code>Socket</code>
     *
     * @throws java.io.IOException timeout is reached or other I/O error occurs
     */
    public static Socket getSocket(String host, int port, long delay)
            throws IOException {
        InetAddress inetAddr = InetAddress.getByName(host);
        return getSocket(inetAddr, port, delay);
    }

    /**
     * Creates a new <code>Socket</code> instance.
     *
     * @param host  the host to connect to
     * @param port  the port to connect to
     * @param delay timeout in milliseconds
     *
     * @return a <code>Socket</code>
     *
     * @throws java.io.IOException timeout is reached or other I/O error occurs
     */
    public static Socket getSocket(String host,
                                   int port,
                                   long delay,
                                   int sendBuffer,
                                   int receiveBuffer)
            throws IOException {
        InetAddress inetAddr = InetAddress.getByName(host);
        return getSocket(inetAddr, port, delay, sendBuffer, receiveBuffer);
    }

    /**
     * Creates a new socket using reflection and connection method.
     *
     * @param addr          The address.
     * @param port          The port.
     * @param timeout       Connection timeout.
     * @param sendBuffer    Send buffer size.
     * @param receiveBuffer Receive buffer size.
     * @param localAddr     Local address.
     * @param localPort     Local port.
     *
     * @return New socket instance.
     *
     * @throws IOException If an error occurs.
     */
    private static Socket createSpecialSocket(InetAddress addr,
                                              int port,
                                              long timeout,
                                              int sendBuffer,
                                              int receiveBuffer,
                                              InetAddress localAddr,
                                              int localPort)
            throws IOException {
        Socket socket = null;

        try {
            Class socketClass = Class.forName("java.net.Socket");
            socket = (Socket) socketClass.newInstance();
            Class socketAddressClass = Class.forName("java.net.SocketAddress");
            Class[] parameterTypes = {socketAddressClass, Integer.TYPE};

            Class[] inetSocketTypes = new Class[2];

            Class c2 = Class.forName("java.net.InetAddress");
            inetSocketTypes[0] = c2;
            inetSocketTypes[1] = Integer.TYPE;
            Class inetSocketAddressClass = Class.forName("java.net.InetSocketAddress");
            Constructor inetSocketContructor = inetSocketAddressClass.getConstructor(inetSocketTypes);

            Object[] inetSocketArgs = new Object[2];

            if (localAddr != null) {
                inetSocketArgs[0] = localAddr;
                inetSocketArgs[1] = new Integer(localPort);
                Object bindInetSocketInstance = inetSocketContructor.newInstance(inetSocketArgs);
                Object[] bindArgs = new Object[1];
                bindArgs[0] = bindInetSocketInstance;
                Class[] bindParameterTypes = new Class[1];
                bindParameterTypes[0] = socketAddressClass;
                Method bindMethod = socketClass.getMethod("bind", bindParameterTypes);
                bindMethod.invoke(socket, bindArgs);
            }

            inetSocketArgs[0] = addr;
            inetSocketArgs[1] = new Integer(port);

            Object inetSocketInstance = inetSocketContructor.newInstance(inetSocketArgs);

            Object[] connectArgs = new Object[2];
            connectArgs[0] = inetSocketInstance;
            connectArgs[1] = new Integer((int) timeout);

            Method connectMethod = socketClass.getMethod("connect", parameterTypes);
            connectMethod.invoke(socket, connectArgs);

            if (sendBuffer > 0) {
                socket.setSendBufferSize(sendBuffer);
            }

            if (receiveBuffer > 0) {
                socket.setReceiveBufferSize(receiveBuffer);
            }

            return socket;
        } catch (InvocationTargetException e) {
            throw IOTools.ioException(e.getTargetException());
        } catch (Exception e) {
            throw IOTools.ioException(e);
        }
    }

    /**
     * This sub process will be sleeping while the connection is
     * created. If the time out is reached the thread will be stopped.
     *
     * @author Alvaro
     */
    static class SleepThread
            extends Thread {

        private boolean stop;
        private long delay;

        SleepThread(boolean stop, long delay) {
            this.stop = stop;
            this.delay = delay;
        }

        public void setStop(boolean stop) {
            this.stop = stop;
            if (stop) {
                try {
                    interrupt();
                } catch (Exception e) {
                    // todo: ignore?
                }
            }
        }

        public boolean getStop() {
            return stop;
        }

        public long getDelay() {
            return delay;
        }

        public void setDelay(long delay) {
            this.delay = delay;
        }

        public void run() {
            try {
                sleep(delay);
                stop = true;
            } catch (InterruptedException e) {
                stop = true;
            }
        }
    }

    private TimedSocket() {
    }

    static class SocketThread
            extends Thread {

        // Socket connection to remote host
        private volatile Socket connection = null;
        // internet address to connect to
        private InetAddress inet = null;
        // port number to connect to
        private int port = 0;
        // Exception in the event a connection error occurs
        private IOException exception = null;
        // timeout of thread
        private long timeout = 0;

        private InetAddress local = null;

        private int localPort = 0;

        private boolean running = false;

        private int sendBuffer = -1;

        private int receiveBuffer = -1;

        private boolean useProxy = false;
        private String proxyHostname;
        private int proxyPort = 1080;
        private String proxyUsername;
        private String proxyPassword;
        private String proxyType = "SOCKS5";

        private String host = null;

        private SleepThread sleepThread = null;

        // Connect to the specified host IP and port number
        SocketThread(InetAddress inet, int port, long timeout) {
            // Assign to member variables
            this.inet = inet;
            this.port = port;
            this.timeout = timeout;
        }

        SocketThread(String host, int port, long timeout) {
            // Assign to member variables
            this.host = host;
            this.port = port;
            this.timeout = timeout;
        }


        // Connect to the specified host IP and port number
        SocketThread(InetAddress inet, int port, long timeout, int sendBuffer, int receiveBuffer) {
            // Assign to member variables
            this.inet = inet;
            this.port = port;
            this.timeout = timeout;
            this.sendBuffer = sendBuffer;
            this.receiveBuffer = receiveBuffer;
        }

        SocketThread(InetAddress inet, int port, InetAddress local, int localPort, long timeout) {
            this.inet = inet;
            this.port = port;
            this.local = local;
            this.localPort = localPort;
            this.timeout = timeout;
        }

        SocketThread(InetAddress inet,
                     int port,
                     InetAddress local,
                     int localPort,
                     long timeout,
                     int sendBuffer,
                     int receiveBuffer) {
            this.inet = inet;
            this.port = port;
            this.local = local;
            this.localPort = localPort;
            this.timeout = timeout;
            this.sendBuffer = sendBuffer;
            this.receiveBuffer = receiveBuffer;
        }

        // Connect to the specified host IP and port number using a proxy server
        SocketThread(InetAddress inet,
                     int port,
                     long timeout,
                     String proxyHostname,
                     int proxyPort,
                     String proxyUsername,
                     String proxyPassword,
                     String proxyType) {
            // Assign to member variables
            this(inet, port, timeout);
            this.useProxy = true;
            this.proxyHostname = proxyHostname;
            this.proxyUsername = proxyUsername;
            this.proxyPort = proxyPort;
            this.proxyPassword = proxyPassword;
            this.proxyType = proxyType;
        }

        //   Connect to the specified host IP and port number using a proxy server
        SocketThread(String host,
                     int port,
                     long timeout,
                     String proxyHostname,
                     int proxyPort,
                     String proxyUsername,
                     String proxyPassword,
                     String proxyType) {
            // Assign to member variables
            this(host, port, timeout);
            this.useProxy = true;
            this.proxyHostname = proxyHostname;
            this.proxyUsername = proxyUsername;
            this.proxyPort = proxyPort;
            this.proxyPassword = proxyPassword;
            this.proxyType = proxyType;
        }


        // thread checks for connection
        public void run() {

            Socket sock = null;
            running = true;

            while (running) {
                try {
                    if (local == null) {
                        if (!useProxy) {
                            sock = new Socket(inet, port);
                        } else {
                            sock = getSocketUsingProxy();
                        }
                        if (sock != null) {
                            setBufferSizes(sock);
                            running = false;
                            break;
                        }
                    } else {
                        sock = new Socket(inet, port, local, localPort);
                        this.setBufferSizes(sock);
                        running = false;
                        break;
                    }
                } catch (ConnectException ioe) {
                    // do nothing continue
                } catch (IOException ioe) {
                    exception = ioe;
                    running = false;
                    break;
                } catch (Exception ex) {
                    ex.printStackTrace();
                    exception = new IOException(ex.getMessage());
                    // exception = ex;
                    running = false;
                    break;
                }
            }
            connection = sock;
            this.sleepThread.setStop(true);
        }

        /**
         * Create a new proxy socket.
         *
         * @return New socket instance
         *
         * @throws Exception If an error occurs.
         */
        public Socket getSocketUsingProxy()
                throws Exception {
            Socket sock = null;
            Proxy prx = null;
            if (proxyType == null) {
                proxyType = "SOCKS5";
            }

            if (proxyType.equals("HTTP")) {
                ProxyHTTP hprx = new ProxyHTTP(proxyHostname, proxyPort);
                hprx.setUserPasswd(proxyUsername, proxyPassword);
                prx = hprx;
            } else if (proxyType.equals("SOCKS4")) {
                ProxySOCKS4 sprx = new ProxySOCKS4(proxyHostname, proxyPort);
                sprx.setUserPasswd(proxyUsername, proxyPassword);
                prx = sprx;
            } else if (proxyType.equals("SOCKS5")) {
                ProxySOCKS5 sprx = new ProxySOCKS5(proxyHostname, proxyPort);
                sprx.setUserPasswd(proxyUsername, proxyPassword);
                prx = sprx;
            } else {
                throw new Exception("Invalid proxy type. Valid proxy types are: HTTP, SOCKS4, SOCKS5");
            }
            String host = null;
            if (inet == null) {
                host = this.host;
            } else {
                host = inet.getHostAddress();
            }

            try {
                prx.connect(null, host, port, timeout);
                sock = prx.getSocket();
            } catch (Exception e) {
                sock = null;
            }
            return sock;
        }

        /**
         * Checks if client is connected
         *
         * @return connection status
         */
        public boolean isConnected() {
            return connection != null;
        }

        public boolean isRunning() {
            return running;
        }

        public void setRunning(boolean running) {
            this.running = running;
        }

        // check for error
        public boolean isError() {
            return (exception != null);
        }

        // get socket
        public Socket getSocket() {
            return connection;
        }

        // get exception
        public IOException getException() {
            return exception;
        }

        // sets the buffer sizes
        private void setBufferSizes(Socket sock)
                throws SocketException {

            if (this.sendBuffer > 0) {
                sock.setSendBufferSize(this.sendBuffer);
            }

            if (this.receiveBuffer > 0) {
                sock.setReceiveBufferSize(this.receiveBuffer);
            }
        }

        /**
         * Store the sleep thread instance.
         *
         * @param sleepThread Sleep thread instance
         */
        private void setSleepThread(SleepThread sleepThread) {
            this.sleepThread = sleepThread;
        }

    }
}
