/*
 * @(#)IpClient.java
 *
 * Copyright (c) 2003 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.ipclient;

import com.jscape.inet.util.TimedSocket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Vector;


/**
 * Implements basic functionality of a TCP/IP client connection. <P> Example
 * usage: <P>
 * <pre>
 * try {
 *   // create new IpClient instance with hostname, port and timeout arguments
 *   IpClient client = new IpClient("10.0.0.1",53,5000);
 *
 *   // establish connection with server
 *   client.connect();
 *
 *   // obtain OutputStream for sending data to server
 *   OutputStream out = client.getOutputStream();
 *
 *   // obtain InputStream for reading data from server
 *   InputStream in = client.getInputStream();
 *
 *   // disconnect from server
 *   client.disconnect();
 *
 * } catch(IpClientException ipe) {
 *   ipe.printStackTrace();
 * }
 *
 * </pre>
 */
@SuppressWarnings("unchecked")
public class IpClient
        implements AutoCloseable {

    protected InputStream input = null;
    protected OutputStream output = null;
    protected Socket socket = null;
    protected String hostname = null;
    protected int connectTimeout = 30000;
    protected int port = 0;
    protected int readTimeout = 30000;
    protected int timeout = 0;
    private Vector listeners = null;

    private static final int DEFAULT_PROXY_PORT = 1080;
    private static final String DEFAULT_PROXY_TYPE = "SOCKS5";
    private String proxyHostname = null;
    private int proxyPort = DEFAULT_PROXY_PORT;
    private String proxyUsername = null;
    private String proxyPassword = null;
    private String proxyType = DEFAULT_PROXY_TYPE;

    /**
     * Creates a new IpClient instance.
     *
     * @param hostname the server hostname or IP address
     * @param port     the server port
     */
    public IpClient(String hostname, int port) {
        
        this.hostname = hostname;
        this.port = port;
        listeners = new Vector();
    }

    /**
     * Creates a new IpClient instance.
     *
     * @param hostname the server hostname or IP address
     * @param port     the server port
     * @param timeout  the timeout for establishing a connection specified in ms (milliseconds)
     */
    public IpClient(String hostname, int port, int timeout) {
        this(hostname, port);
        this.timeout = timeout;
        this.connectTimeout = timeout;
    }

    /**
     * Sets the timeout for opening a server connection.
     * <p/>
     * Default is 30 seconds.
     *
     * @param timeout the timeout in milliseconds
     */
    public void setConnectTimeout(int timeout) {
        this.connectTimeout = timeout;
    }

    /**
     * Gets the timeout used when opening a server connection.
     * <p/>
     * Default is 30 seconds.
     *
     * @return timeout server connection.
     */
    public int getConnectTimeout() {
        return this.connectTimeout;
    }

    /**
     * Replaces InputStream from socket connection established with server.
     * IpClient#connect method must be invoked prior to invoking this method.
     *
     * @param input The input stream instance
     */
    public void setInputStream(InputStream input) {
        synchronized (this) {
            this.input = input;
        }
    }

    /**
     * Gets InputStream from socket connection established with server.
     * IpClient#connect method must be invoked prior to invoking this method.
     *
     * @return an InputStream or null if connection has been closed or not yet
     *         established
     */
    public InputStream getInputStream() {
        return input;
    }

    /**
     * Sets OutputStream from socket connection established with server.
     *
     * @param output The output stream instance
     */
    public void setOutputStream(OutputStream output) {
        synchronized (this) {
            this.output = output;
        }
    }

    /**
     * Gets OutputStream from socket connection established with server.
     * IpClient#connect method must be invoked prior to invoking this method.
     *
     * @return an OutputStream or null if connection has been closed or not yet
     *         established
     */
    public OutputStream getOutputStream() {
        return output;
    }

    /**
     * Sets the timeout for reading responses from server.
     * Default is 30 seconds.
     *
     * @param timeout the timeout in milliseconds
     */
    public void setReadTimeout(int timeout) {
        this.readTimeout = timeout;
    }

    /**
     * Gets the timeout used when reading responses from server.
     * Default is 30 seconds.
     *
     * @return reading from server timeout.
     */
    public int getReadTimeout() {
        return this.readTimeout;
    }

    /**
     * Sets socket replacing current socket with new socket.
     *
     * @param socket the new socket
     *
     * @throws IpClientException if a I/O error occurs.
     */
    public void setSocket(Socket socket)
            throws IpClientException {
        synchronized (this) {
            try {
                this.socket = socket;
                this.input = socket.getInputStream();
                this.output = socket.getOutputStream();
                this.hostname = socket.getInetAddress().getHostAddress();
                this.port = socket.getPort();
            } catch (IOException e) {
                throw new IpClientException(e.getMessage(), e);
            }
        }
    }

    /**
     * Gets Socket established after invoking the IpClient#connect method.
     *
     * @return a Socket
     *
     * @see #connect
     */
    public Socket getSocket() {
        return socket;
    }

    /**
     * Sets the username and password to use when for authentication with
     * proxy server.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyUsername the proxy username
     * @param proxyPassword the proxy password
     *
     * @see #clearProxySettings
     */
    public void setProxyAuthentication(String proxyUsername,
                                       String proxyPassword) {
        this.proxyUsername = proxyUsername;
        this.proxyPassword = proxyPassword;
    }

    // PROXY CONTROL

    /**
     * Sets the proxy hostname and port for this connection.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyHostname the hostname or ip address of the proxy server
     * @param proxyPort     the port of the proxy server
     *
     * @see #clearProxySettings
     */
    public void setProxyHost(String proxyHostname, int proxyPort) {
        this.proxyHostname = proxyHostname;
        this.proxyPort = proxyPort;
    }

    /**
     * Sets the proxy type will be used for this connection.
     *
     * @param proxyType The proxy type. Valid values: HTTP, SOCKS5
     */
    public void setProxyType(String proxyType) {
        this.proxyType = proxyType;
    }

    /**
     * Clears proxy server values.
     */
    public void clearProxySettings() {
        setProxyAuthentication(null, null);
        setProxyHost(null, DEFAULT_PROXY_PORT);
        setProxyType(DEFAULT_PROXY_TYPE);
    }

    /**
     * Adds IpClient event listener.
     *
     * @param listener an IpClientListener
     */
    public synchronized void addIpClientListener(IpClientListener listener) {
        listeners.addElement(listener);
    }

    /**
     * Establishes connection with server.
     *
     * @throws IpClientException if an I/O or network related error occurs
     */
    public IpClient connect()
            throws IpClientException {
        synchronized (this) {
            try {
                if (this.proxyHostname == null) {
                    socket = TimedSocket.getSocket(hostname, port, connectTimeout);
                } else {
                    socket = TimedSocket.getSocket(hostname,
                                                   port,
                                                   connectTimeout,
                                                   this.proxyHostname,
                                                   this.proxyPort,
                                                   this.proxyUsername,
                                                   this.proxyPassword,
                                                   this.proxyType);
                }

                if (this.readTimeout > 0) {
                    this.socket.setSoTimeout(this.readTimeout);
                }

                input = socket.getInputStream();
                output = socket.getOutputStream();
                fireIpConnected();

                return this;
            } catch (Exception e) {
                throw new IpClientException(e.getMessage(), e);
            }
        }
    }

    /**
     * Closes connection with server.
     *
     * @throws IpClientException if an I/O or network related error occurs
     */
    public void disconnect()
            throws IpClientException {
        synchronized (this) {
            try {
                socket.close();
                fireIpDisconnected();
            } catch (Exception e) {
                throw new IpClientException(e.getMessage(), e);
            }
        }
    }

    @Override
    public void close()
            throws Exception {
        disconnect();
    }

    /**
     * Removes an IpClient event listener.
     *
     * @param listener an IpClientListener
     */
    public synchronized void removeIpClientListener(IpClientListener listener) {
        listeners.removeElement(listener);
    }

    /**
     * Fires <code>IpClientConnectedEvent</code>. This event is fired upon
     * establishing a connection with the server.
     *
     * @see IpClientConnectedEvent
     */
    protected void fireIpConnected() {
        Vector thisList;
        IpClientConnectedEvent thisEvent = new IpClientConnectedEvent(this,
                                                                      hostname, port);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        // Send a IpClientConnectedEvent to every listener.
        for (int elem = 0; elem < thisList.size(); elem++) {
            ((IpClientListener) thisList.elementAt(elem)).connected(thisEvent);
        }
    }

    /**
     * Fires <code>IpClientDisconnectedEvent</code>. This event is fired upon
     * closing connection with the server.
     *
     * @see IpClientDisconnectedEvent
     */
    protected void fireIpDisconnected() {
        Vector thisList;
        IpClientDisconnectedEvent thisEvent = new IpClientDisconnectedEvent(this,
                                                                            hostname, port);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        // Send a IpClientDisconnectedEvent to every listener.
        for (int elem = 0; elem < thisList.size(); elem++) {
            ((IpClientListener) thisList.elementAt(elem)).disconnected(thisEvent);
        }
    }
}
