package org.apache.log4j.net;

import java.net.InetAddress;
import java.net.Socket;
import java.io.IOException;
import java.io.DataOutputStream;

import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.AppenderSkeleton;

public class SocketTextAppender
       extends AppenderSkeleton
{
  /**
     A string constant used in naming the option for setting the the
     host name of the remote server.  Current value of this string
     constant is <b>RemoteHost</b>. See the {@link #setOption} method
     for the meaning of this option.

  */
  public static final String REMOTE_HOST_OPTION = "RemoteHost";

 /**
     A string constant used in naming the option for setting the the
     port to contect on the remote server.  Current value of this string
     constant is <b>Port</b>.  See the {@link #setOption} method
     for the meaning of this option.

  */
  public static final String PORT_OPTION = "Port";

  /**
     A string constant used in naming the option for setting the the
     location information flag.  Current value of this string
     constant is <b>LocationInfo</b>.  See the {@link #setOption} method
     for the meaning of this option.

  */
  public static final String LOCATION_INFO_OPTION = "LocationInfo";

  /**
     A string constant used in naming the option for setting the delay
     between each reconneciton attempt to remote server.  Current
     value a of this string constant is <b>ReconnectionDelay</b>.  See
     the {@link #setOption} method for the meaning of this option.

  */
  public static final String RECONNECTION_DELAY_OPTION = "ReconnectionDelay";

  /**
     The default port number of remote logging server (4560).
  */
  static final int DEFAULT_PORT                 = 4560;
  
  /**
     The default reconnection delay (30000 milliseconds or 30 seconds).
  */
  static final int DEFAULT_RECONNECTION_DELAY   = 30000;

  /**
     We remember host name as String in addition to the resolved
     InetAddress so that it can be returned via getOption().
  */
  
	public static String remoteHost;

	InetAddress			address;
	int					port				= DEFAULT_PORT;
	DataOutputStream	oos;
	int					reconnectionDelay	= DEFAULT_RECONNECTION_DELAY;

	boolean locationInfo = false;

	private Connector connector;

	// reset the ObjectOutputStream every 70 calls
	//private static final int RESET_FREQUENCY = 70;
	private static final int RESET_FREQUENCY = 1;
  
	public SocketTextAppender() {}

  /**
     Connects to remote server at <code>address</code> and <code>port</code>.
  */
	public SocketTextAppender(InetAddress address, int port) {
    this.address = address;
    this.remoteHost = address.getHostName();
    this.port = port;
    connect(address, port);
  }

  /**
     Connects to remote server at <code>host</code> and <code>port</code>.
  */
	public SocketTextAppender(String host, int port) { 
    this.port = port;
    this.address = getAddressByName(host);
    this.remoteHost = host;
    connect(address, port);
  }


  /**
     The <b>RemoteHost</b> option takes a string value which should be
     the host name of the server where a {@link SocketNode} is running.
   */
  public
  void setRemoteHost(String host) {
    address = getAddressByName(host);
    remoteHost = host;
  }
  
  /**
     Returns value of the <b>RemoteHost</b> option.
   */
  public
  String getRemoteHost() {
    return remoteHost;
  }
  
  /**
     The <b>Port</b> option takes a positive integer representing
     the port where the server is waiting for connections.
   */
  public
  void setPort(int port) {
    this.port = port;
  }
  
  /**
     Returns value of the <b>Port</b> option.
   */
  public
  int getPort() {
    return port;
  }
  
  /**
     The <b>LocationInfo</b> option takes a boolean value. If true,
     the information sent to the remote host will include location
     information. By default no location information is sent to the server.
   */
  public
  void setLocationInfo(boolean locationInfo) {
    this.locationInfo = locationInfo;
  }
  
  /**
     Returns value of the <b>LocationInfo</b> option.
   */
  public
  boolean getLocationInfo() {
    return locationInfo;
  }
  
  /**
     The <b>ReconnectionDelay</b> option takes a positive integer
     representing the number of milliseconds to wait between each
     failed connection attempt to the server. The default value of
     this option is 30000 which corresponds to 30 seconds.
     
     <p>Setting this option to zero turns off reconnection
     capability.
   */
  public
  void setReconnectionDelay(int delay) {
    this.reconnectionDelay = delay;
  }
  
  /**
     Returns value of the <b>ReconnectionDelay</b> option.
   */
  public
  int getReconnectionDelay() {
    return reconnectionDelay;
  }  
  
  /**
     Connect to the specified <b>RemoteHost</b> and <b>Port</b>.
  */
  public void activateOptions()
  {
    connect( address, port );
  }

  /**
     Close this appender.
     <p>This will mark the appender as closed and
     call then {@link #cleanUp} method.
  */
  public void close()
  {
    this.closed = true;
    cleanUp();
  }

  /**
     Drop the connection to the remote host and release the underlying
     connector thread if it has been created
   */
  public void cleanUp()
  {
    if( oos != null ) {
      try
      {
        oos.close();
      }
      catch( IOException e )
      {
        LogLog.error( "Could not close oos.", e );
      }
      oos = null;
    }
    if( connector != null ) {
      connector.interrupt();
      connector = null;
    }
  }

  void connect( InetAddress address, int port )
  {
    if( this.address == null ) {
      return;
    }
    try
    {
      // First, close the previous connection if any.
      cleanUp();
      oos = new DataOutputStream( new Socket( address, port ).getOutputStream() );
    }
    catch( IOException e )
    {
      LogLog.error( "Could not connect to remote socket server at '" + address.getHostName() + "'.  Will try again later.", e);
      fireConnector();
    }
  }

  public void append( LoggingEvent event )
  {
    if( address == null )
    {
      errorHandler.error( "No remote host is set for SocketAppedender named '" + this.name + "'" );
      return;
    }

    if( layout == null ) {
      errorHandler.error( "No layout set for appender named '" + name + "'");
      return;
    }

    if( oos != null ) {
      try
      {
        // write the object using the format string
        oos.writeBytes( layout.format( event ) );
				oos.flush();
      }
      catch( IOException e )
      {
				oos = null;
				LogLog.debug( "Detected problem with connection:  " + e );
				fireConnector();
      }
    }
  }

  void fireConnector()
  {
    if( connector == null ) {
      LogLog.debug( "Starting a new connector thread." );
      connector = new Connector();
      connector.setDaemon( true );
      connector.setPriority( Thread.MIN_PRIORITY );
      connector.start();
    }
  }

  InetAddress getAddressByName( String host )
  {
    try
    {
      return( InetAddress.getByName( host ) );
    }
    catch( Exception e )
    {
      LogLog.error( "Could not find address of ["+host+"].", e );
      return null;
    }
  }

  /**
     The SocketTextAppender uses a layout. Hence, this method returns
     <code>false</code>.
  */
  public boolean requiresLayout()
  {
    return true;
  }

  /**
     The Connector will reconnect when the server becomes available
     again.  It does this by attempting to open a new connection every
     <code>reconnectionDelay</code> milliseconds.

     <p>It stops trying whenever a connection is established. It will
     restart to try reconnect to the server when previpously open
     connection is droppped.

     @author  Ceki G&uuml;lc&uuml;
     @since 0.8.4
  */
  class Connector
        extends Thread
  {
    public void run()
    {
      Socket socket;

      while( !isInterrupted() )
      {
        try
        {
          sleep( reconnectionDelay );
          LogLog.debug( "Attempting connection to '" + address.getHostName() + "'" );
          socket = new Socket( address, port );
	        synchronized( this )
          {
            oos = new DataOutputStream( socket.getOutputStream() );
            connector = null;
            break;
          }
        }
        catch( InterruptedException e )
        {
          LogLog.debug( "Connector interrupted. Leaving loop." );
          return;
        }
		    catch( java.net.ConnectException e )
	      {
	        LogLog.debug( "Remote host '" + address.getHostName() + "' refused connection." );
		    }
	      catch( IOException e )
	      {
	        LogLog.debug( "Could not connect to '" + address.getHostName() + "'. Exception is: " + e );
	      }
      }
    }
  }
}
