/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
 * Copyright (C) 2012 Brian P. Hinz
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 * USA.
 */

package com.tigervnc.network;

import com.tigervnc.rdr.FdInStream;
import com.tigervnc.rdr.FdOutStream;
import com.tigervnc.rdr.Exception;
import com.tigervnc.rfb.LogWriter;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.*;
import java.nio.channels.*;

import java.util.Set;
import java.util.Iterator;

public class TcpSocket extends Socket {

  // -=- Socket initialisation
  public static boolean socketsInitialised = false;
  public static void initSockets() {
    if (socketsInitialised)
      return;
    socketsInitialised = true;
  }

  // -=- TcpSocket

  public TcpSocket(SocketDescriptor sock, boolean close) {
    super(new FdInStream(sock), new FdOutStream(sock), true);
    closeFd = close;
  }

  public TcpSocket(SocketDescriptor sock) {
    this(sock, true);
  }

  public TcpSocket(String host, int port) throws Exception {
    closeFd = true;
    SocketDescriptor sock = null;
    InetAddress addr = null;
    boolean result = false;

    // - Create a socket
    initSockets();

    try {
      addr = java.net.InetAddress.getByName(host);
    } catch(UnknownHostException e) {
      throw new Exception("unable to resolve host by name: "+e.toString());
    }

    try {
      sock = new SocketDescriptor();
    } catch(Exception e) {
      throw new SocketException("unable to create socket: "+e.toString());
    }

    /* Attempt to connect to the remote host */
    try {
      result = sock.connect(new InetSocketAddress(addr, port));
      Selector selector = Selector.open();
      SelectionKey connect_key =
        sock.socket().getChannel().register(selector, SelectionKey.OP_CONNECT);
      // Try for the connection for 250ms
      while (selector.select(250) > 0) {
        while (!result) {

          Set keys = selector.selectedKeys();
          Iterator i = keys.iterator();

          while (i.hasNext()) {
            SelectionKey key = (SelectionKey)i.next();

            // Remove the current key
            i.remove();

            // Attempt a connection
            if (key.isConnectable()) {
              if (sock.isConnectionPending())
                sock.finishConnect();
              result = true;
            }
          }
        }
      }
      if (!result)
        throw new SocketException("unable to connect to socket: Host is down");
    } catch(java.io.IOException e) {
      throw new SocketException("unable to connect:"+e.getMessage());
    }

    // Disable Nagle's algorithm, to reduce latency
    enableNagles(sock, false);

    // Create the input and output streams
    instream = new FdInStream(sock);
    outstream = new FdOutStream(sock);
    ownStreams = true;
  }

  protected void finalize() throws Exception {
    if (closeFd)
      try {
        ((SocketDescriptor)getFd()).close();
      } catch (IOException e) {
        throw new Exception(e.getMessage());
      }
  }

  public int getMyPort() {
    return getSockPort();
  }

  public String getPeerAddress() {
    InetAddress peer = ((SocketDescriptor)getFd()).socket().getInetAddress();
    if (peer != null)
      return peer.getHostAddress();
    return "";
  }

  public String getPeerName() {
    InetAddress peer = ((SocketDescriptor)getFd()).socket().getInetAddress();
    if (peer != null)
      return peer.getHostName();
    return "";
  }

  public int getPeerPort() {
    int port = ((SocketDescriptor)getFd()).socket().getPort();
    return port;
  }

  public String getPeerEndpoint() {
    String address = getPeerAddress();
    int port = getPeerPort();
    return address+"::"+port;
  }

  public boolean sameMachine() throws Exception {
    try {
      SocketAddress peeraddr = ((SocketDescriptor)getFd()).getRemoteAddress();
      SocketAddress myaddr = ((SocketDescriptor)getFd()).getLocalAddress();
      return myaddr.equals(peeraddr);
    } catch (IOException e) {
      throw new Exception(e.getMessage());
    }
  }

  public void shutdown() throws Exception {
    super.shutdown();
    try {
      ((SocketDescriptor)getFd()).shutdown();
    } catch (IOException e) {
      throw new Exception(e.getMessage());
    }
  }

  public void close() throws IOException {
    ((SocketDescriptor)getFd()).close();
  }

  public static boolean enableNagles(SocketDescriptor sock, boolean enable) {
    try {
      sock.channel.socket().setTcpNoDelay(!enable);
    } catch(java.net.SocketException e) {
      vlog.error("unable to setsockopt TCP_NODELAY: "+e.getMessage());
      return false;
    }
    return true;
  }

  public static boolean isSocket(java.net.Socket sock) {
    return sock.getClass().toString().equals("com.tigervnc.net.Socket");
  }

  public boolean isConnected() {
    return ((SocketDescriptor)getFd()).isConnected();
  }

  public int getSockPort() {
    return ((SocketDescriptor)getFd()).socket().getLocalPort();
  }

  /* Tunnelling support. */
  public static int findFreeTcpPort() {
    java.net.ServerSocket sock;
    int port;
    try {
      sock = new java.net.ServerSocket(0);
      port = sock.getLocalPort();
      sock.close();
    } catch (java.io.IOException e) {
      throw new SocketException("unable to create socket: "+e.toString());
    }
    return port;
  }

  private boolean closeFd;
  static LogWriter vlog = new LogWriter("TcpSocket");

}


