package net.kanstren.tcptunnel.forwarder;

import net.kanstren.tcptunnel.Main;
import net.kanstren.tcptunnel.Params;
import net.kanstren.tcptunnel.Utils;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Creates a TCP tunnel between two endpoints via two Forwarder instances.
 * Data is forwarded in both directions using separate sockets.
 * Any error on either socket causes the whole tunnel (both sockets) to be closed.
 */
public class TCPTunnel extends Thread {
  private SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MMM.dd HH:mm:ss");
  /** Configuration parameters. */
  private final Params params;
  /** Local endpoint for the tunnel. */
  private Socket localSocket;
  /** Remote endpoint for the tunnel. */
  private Socket serverSocket;
  /** True if this tunnel is actively forwarding. False if stopped or not yet started. */
  private boolean active = false;
  /** Parent to notify when connection is broken. */
  private final Main parent;

  /**
   * @param params Configuration parameters.
   * @param localSocket Socket for the local port (endpoint 1 for tunnel).
   */
  public TCPTunnel(Params params, Socket localSocket, Main parent) {
    this.params = params;
    this.localSocket = localSocket;
    this.parent = parent;
  }

  /**
   * Connects to the remote host and starts bidirectional forwarding (the tunnel).
   */
  public void run() {
    String dateStr = sdf.format(new Date());
    try {
      // Connect to the destination server
      serverSocket = new Socket(params.getRemoteHost(), params.getRemotePort());

      // Turn on keep-alive for both the sockets
      serverSocket.setKeepAlive(true);
      localSocket.setKeepAlive(true);

      // Obtain client & server input & output streams
      InputStream clientIn = localSocket.getInputStream();
      OutputStream clientOut = localSocket.getOutputStream();
      InputStream serverIn = serverSocket.getInputStream();
      OutputStream serverOut = serverSocket.getOutputStream();

      // Start forwarding data between server and client
      active = true;
      String clientAddr = toStr(localSocket);
      String serverAddr = toStr(serverSocket);
      String hummanClientAddr = Utils.mapAddrToHumanReadable(clientAddr);
      String hummanServerAddr = Utils.mapAddrToHumanReadable(serverAddr);
      clientAddr = clientAddr+" ("+hummanClientAddr+")";
      serverAddr = serverAddr+" ("+hummanServerAddr+")";
      TCPForwarder clientForward = new TCPForwarder(this, clientIn, serverOut, params, true, clientAddr);
      clientForward.start();
      TCPForwarder serverForward = new TCPForwarder(this, serverIn, clientOut, params, false, serverAddr);
      serverForward.start();

      if (params.isPrint()) {
        System.out.println(dateStr+": TCP Forwarding " + clientAddr + " <--> " + serverAddr);
      }
    } catch (IOException ioe) {
      if (params.isPrint()) {
        String remoteAddr = params.getRemoteHost() + ":" + params.getRemotePort();
        String humanRemoteAddr = Utils.mapAddrToHumanReadable(remoteAddr);
        remoteAddr = remoteAddr + " ("+humanRemoteAddr+")";
        System.err.println(dateStr + ": Failed to connect to remote host (" + remoteAddr + ")");
      }
      connectionBroken();
    }
  }

  /**
   * @param socket The socket to describe.
   * @return A string representation of a socket (ip+port).
   */
  private String toStr(Socket socket) {
    String host = socket.getInetAddress().getHostAddress();
    int port = socket.getPort();
    return host + ":" + port;
  }

  /**
   * Closes the tunnel (the forwarding sockets..).
   */
  public void close() {
    connectionBroken();
  }

  /**
   * Called when an error is observed on one of the sockets making up the tunnel.
   * Terminates the tunnel by closing both sockets.
   */
  public synchronized void connectionBroken() {
    try {
      serverSocket.close();
    } catch (Exception e) {}
    try {
      localSocket.close();
    } catch (Exception e) {}

    if (active) {
      String dateStr = sdf.format(new Date());
      if (params.isPrint()) System.out.println(dateStr+": TCP Forwarding " + toStr(localSocket) + " <--> " + toStr(serverSocket) + " stopped.");
      active = false;
    }
    parent.closed(this);
  }
}