/**
 * A network library for processing which supports UDP, TCP and Multicast.
 *
 * (c) 2004-2011
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA  02111-1307  USA
 * 
 * @author		Andreas Schlegel http://www.sojamo.de/libraries/oscP5
 * @modified	12/19/2011
 * @version		0.9.8
 */

package netP5;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.io.InputStream;
import java.io.OutputStream;


/**
 * @invisible
 */
public abstract class AbstractTcpClient implements Runnable {

  private Socket _mySocket;

  protected TcpPacketListener _myTcpPacketListener;

  private PrintWriter _myOutput = null;

  private BufferedReader _myInput = null;

  private OutputStream _myOutputStream = null;

  protected byte[] _myBytes = new byte[0];

  protected StringBuffer _myStringBuffer = new StringBuffer(0);

  protected AbstractTcpServer _myTcpServer;

  protected NetAddress _myNetAddress;

  protected int _myServerPort;

  private Thread _myThread;

  private char TERMINATOR = '\0';
  
  /**
   * terminator is readline.
   */
  public static final int MODE_READLINE = 0;

  /**
   * terminator is terminated, by default this is character '\0'
   * and can be set with setTerminator
   */
  public static final int MODE_TERMINATED = 1;

  /**
   * terminator is newline.
   */
  public static final int MODE_NEWLINE = 2;
  
  /**
   * no terminator required, packets are sent via
   * a tcp stream.
   */
  public static final int MODE_STREAM = 3;

  private final int _myMode;

  /**
   * @invisible
   * @param theTcpPacketListener TcpPacketListener
   * @param theHost String
   * @param thePort int
   */
  public AbstractTcpClient(final TcpPacketListener theTcpPacketListener,
                           final String theHost,
                           final int thePort) {
    this(theTcpPacketListener, theHost, thePort, MODE_READLINE);
  }

  /**
   * @invisible
   * @param theHost String
   * @param thePort int
   */
  public AbstractTcpClient(final String theHost,
                           final int thePort) {
    this(null, theHost, thePort, MODE_READLINE);
  }

  /**
   * @invisible
   * @param theTcpPacketListener TcpPacketListener
   * @param theHost String
   * @param thePort int
   * @param theMode int
   */
  public AbstractTcpClient(final TcpPacketListener theTcpPacketListener,
                           final String theHost,
                           final int thePort,
                           final int theMode) {
    _myTcpPacketListener = theTcpPacketListener;
    _myNetAddress = new NetAddress(theHost, thePort);
    _myMode = theMode;
    startSocket();
  }

  /**
   * @invisible
   * @param theHost String
   * @param thePort int
   * @param theMode int
   */
  public AbstractTcpClient(final String theHost,
                           final int thePort,
                           final int theMode) {
    this(null, theHost, thePort, theMode);
  }


  /**
   * @invisible
   * @param theTcpServer AbstractTcpServer
   * @param theSocket Socket
   * @param theTcpPacketListener TcpPacketListener
   * @param theServerPort int
   * @param theMode int
   */
  public AbstractTcpClient(final AbstractTcpServer theTcpServer,
                           final Socket theSocket,
                           final TcpPacketListener theTcpPacketListener,
                           final int theServerPort,
                           final int theMode) {
    _myTcpServer = theTcpServer;
    _mySocket = theSocket;
    _myTcpPacketListener = theTcpPacketListener;
    _myServerPort = theServerPort;
    _myMode = theMode;
    startSocket();
  }


  private void startSocket() {
    try {
      if (_mySocket == null) {
        _mySocket = new Socket(_myNetAddress.address(), _myNetAddress.port());
      } else {
        _myNetAddress = new NetAddress(_mySocket.getInetAddress().getHostAddress(),
                                       _mySocket.getPort());
      }
      Logger.printProcess("TcpClient", "### starting new TcpClient " + _myNetAddress);
      if (_myMode == MODE_STREAM) {
        _myOutputStream = _mySocket.getOutputStream();
      }
      init();
    } catch (final IOException e) {
      Logger.printError("TcpClient",
                        "IOException while trying to create a new socket.");
//      handleStatus(NetStatus.CONNECTION_FAILED); // FIX! NetPlug is still null at this point.  NetPlug has to exist first.
    }
  }


  /**
   * when a TCP connection is lost, reconnect to the server with reconnect().
   */
  public void reconnect() {
    try {
      Thread.sleep(1000);
    } catch(final Exception e) { }
    startSocket();
  }


  private void init() {
    _myThread = new Thread(this);
    _myThread.start();
  }

  /**
   * to parse an incomming tcp message, a terminator character is required to
   * determine the end of the message so that it can be parsed and forwarded.
   *  
   * @param theTerminator
   */
  public void setTerminator(final char theTerminator) {
    TERMINATOR = theTerminator;
  }

  /**
   * stop and dispose a tcp client.
   */
  public void dispose() {
    try {
      // do io streams need to be closed first?
      if (_myInput != null) {
        _myInput.close();
      }
      if (_myOutput != null) {
        _myOutput.close();
      }

    } catch (final Exception e) {
      e.printStackTrace();
    }
    _myInput = null;
    _myOutput = null;

    try {
      if (_mySocket != null) {
        _mySocket.close();
      }

    } catch (final Exception e) {
      e.printStackTrace();
    }
    if(_myThread==null) {
      return;
    }
    _mySocket = null;
    _myThread = null;
    handleStatus(NetStatus.CONNECTION_CLOSED);
    Logger.printProcess("TcpClient.dispose", "TcpClient closed.");
  }

  /**
   * @invisible
   */
  public void run() {
    if (_myMode == MODE_STREAM) {
      try {
        try {
          // sleep a little bit to avoid threading and nullpointer
          // issues when reconnecting.
          _myThread.sleep(500);
        } catch (final Exception e) {

        }

        final InputStream in = _mySocket.getInputStream();
        while (!_mySocket.isClosed() && _mySocket != null) {
          final int myLen = Bytes.toIntBigEndian(in);
          if (myLen < 0) {
            break;
          }
          _myBytes = Bytes.toByteArray(in, myLen);
          handleInput();
        }
      } catch (final java.net.SocketException se) {
        System.out.println("Connection reset.");
      } catch (final Exception e) {
        System.out.println("### EXCEPTION " + e);
      }
      try {
        handleStatus(NetStatus.SERVER_CLOSED);
        handleStatus(NetStatus.CONNECTION_TERMINATED);
        dispose();
      } catch (final NullPointerException e) {
        System.out.println("### nullpointer while calling handleStatus.");
      }
    } else {
      while (Thread.currentThread() == _myThread) {
        switch (_myMode) {
        case (MODE_TERMINATED):
          read();
          break;
        case (MODE_READLINE):
        default:
          readline();
          break;
        }
        break;
      }
    }
    if (_myTcpServer != null) {
      _mySocket = null;
      _myTcpServer.remove(this);
    }
  }


  private void read() {
    try {
      _myInput = new BufferedReader(new InputStreamReader(_mySocket.getInputStream()));

      final char[] charBuffer = new char[1];
      while (_myInput.read(charBuffer, 0, 1) != -1) {

        /**@todo
         * StringBuffer size is limited yet.
         * increase the buffer size dynamically.
         */
        _myStringBuffer = new StringBuffer(4096);
        while (charBuffer[0] != TERMINATOR && charBuffer[0] != 3) {
          _myStringBuffer.append(charBuffer[0]);
          _myInput.read(charBuffer, 0, 1);
        }
        _myBytes = _myStringBuffer.toString().getBytes();
        handleInput();
      }
    } catch (final IOException e) {
      Logger.printProcess("TcpClient.read()", "connection has been terminated.");
      if (_myTcpServer == null) {
        handleStatus(NetStatus.SERVER_CLOSED);
      }
      handleStatus(NetStatus.CONNECTION_TERMINATED);
    }
  }


  private void readline() {
    try {
      _myOutput = new PrintWriter(_mySocket.getOutputStream(), true);
      _myInput = new BufferedReader(new InputStreamReader(_mySocket.getInputStream()));
      String inputLine;

      while ((inputLine = _myInput.readLine()) != null) {
        _myStringBuffer = new StringBuffer(inputLine);
        _myBytes = _myStringBuffer.toString().getBytes();
        handleInput();
      }
    } catch (final IOException e) {
      Logger.printProcess("TcpClient.readline()", "connection has been terminated.");
      handleStatus(NetStatus.CONNECTION_TERMINATED);
      if (_myTcpServer == null) {
        handleStatus(NetStatus.SERVER_CLOSED);
      }
    }
  }

  /**
   * @invisible
   */
  public abstract void handleInput();

  /**
   * @invisible
   * @param theIndex
   */
  public abstract void handleStatus(int theIndex);

  /**
   * @invisible
   * @return
   */
  public TcpPacketListener listener() {
    return _myTcpPacketListener;
  }

  /**
   * get the server port.
   * @return
   */
  public int serverport() {
    return _myServerPort;
  }

  /**
   * get the instance of the socket. more info at java.net.Socket
   * @return
   */
  public Socket socket() {
    return _mySocket;
  }

  
  /**
   * get the mode of the terminator. 
   * @return
   */
  public int mode() {
    return _myMode;
  }


  public String getString() {
    return _myStringBuffer.toString();
  }


  public StringBuffer getStringBuffer() {
    return _myStringBuffer;
  }


  public void send(final byte[] theBytes) {
    if (_myMode == MODE_STREAM) {
      try {
        Bytes.toStream(_myOutputStream, theBytes);
      } catch (final Exception ex) {
        handleStatus(NetStatus.SEND_FAILED);
      }
    } else {
      System.out.println("### sending bytes is only supported for STREAMs");
    }
  }


  public void send(final byte[][] theBytes) {
    if (_myMode == MODE_STREAM) {
      try {
        for (int i = 0; i < theBytes.length; i++) {
          Bytes.toStream(_myOutputStream, theBytes[i]);
        }
      } catch (final Exception ex) {
        handleStatus(NetStatus.SEND_FAILED);
      }
    } else {
      System.out.println("### sending bytes is only supported for STREAMs");
    }

  }


  public void send(final String theString) {
    if (_myMode == MODE_STREAM) {
      send(theString.getBytes());
    } else {
      switch (_myMode) {
      case (MODE_TERMINATED):
        _myOutput.write(theString + TERMINATOR);
        break;
      case (MODE_NEWLINE):
        _myOutput.write(theString + "\n");
        break;
      case (MODE_READLINE):
      default:
        _myOutput.println(theString);
        break;
      }
      _myOutput.flush();
    }
  }


  public NetAddress netAddress() {
    return _myNetAddress;
  }


  /**
   * @deprecated
   * @invisible
   * @return NetAddress
   */

  public NetAddress netaddress() {
    return _myNetAddress;
  }


  /**
   * @param theNetAddress NetAddress
   * @return boolean
   */
  public boolean equals(final NetAddress theNetAddress) {
    if (theNetAddress.address().equals(_myNetAddress.address()) &&
        theNetAddress.port() == _myNetAddress.port()) {
      return true;
    }
    return false;
  }


  public boolean equals(final TcpClient theClient) {
    return equals(theClient.netAddress());
  }

}
