package org.log4j.net;

import java.net.InetAddress;
import java.net.Socket;
import java.io.IOException;
import java.io.DataOutputStream;

import org.log4j.helpers.LogLog;
import org.log4j.helpers.OptionConverter;
import org.log4j.spi.LoggingEvent;
import org.log4j.AppenderSkeleton;

public class SocketTextAppender
       extends AppenderSkeleton
{
  private Connector        connector          = null;
          DataOutputStream oos                = null;
          InetAddress      address            = null;
          int              port               = 4560;
          String           hostName           = null;
          int              reconnectionDelay  = 30000;

  /**
     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";

  public SocketTextAppender() {}

  /**
     Connects to remote server at <code>address</code> and <code>port</code>.
  */
  public SocketTextAppender( InetAddress address, int port )
  {
    this.address = address;
    this.port = port;
    this.hostName = address.getHostName();
    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.hostName = host;
    this.address = getAddressByName( host );
    connect( address, port );
  }

  /**
     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;
    }
  }

  /**
     Retuns the option names for this component, namely the string
     array consisting of {{@link #REMOTE_HOST_OPTION}, {@link
     #PORT_OPTION}, {@link #LOCATION_INFO_OPTION}, {@link
     #RECONNECTION_DELAY_OPTION}} in addition to the options of its
     super class {@link AppenderSkeleton}.

    */
  public String[] getOptionStrings()
  {
    return( OptionConverter.concatanateArrays( super.getOptionStrings(), new String[] { REMOTE_HOST_OPTION, PORT_OPTION, LOCATION_INFO_OPTION, RECONNECTION_DELAY_OPTION } ) );
  }


  /**
     The SocketTextAppender uses a layout. Hence, this method returns
     <code>false</code>.
  */
  public boolean requiresLayout()
  {
    return true;
  }


  /**
     Set SocketAppender specific options.

     <p>On top of the options of the super class {@link
     AppenderSkeleton}, the recognized options are <b>RemoteHost</b>,
     <b>Port</b> and <b>ReconnectionDelay</b>, i.e. the values of the
     string constants {@link #REMOTE_HOST_OPTION}, {@link
     #PORT_OPTION},{@link #LOCATION_INFO_OPTION} and respectively {@link
     #RECONNECTION_DELAY_OPTION}.

     <p>The <b>RemoteHost</b> option takes a string value which should be
     the host name of the server where a {@link SocketNode} is running.

     <p>The <b>Port</b> option takes a positive integer representing
     the port where the server is waiting for connections.

     <p>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.

     <p>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.

   */
  public void setOption( String option, String value )
  {
    if ( value == null ) {
      return;
    }
    super.setOption( option, value );

    if( option.equals( REMOTE_HOST_OPTION ) ) {
      address = getAddressByName(value);
    }
    else if ( option.equals( PORT_OPTION ) ) {
      port = OptionConverter.toInt(value, port);
    }
    else if ( option.equals( RECONNECTION_DELAY_OPTION ) ) {
      reconnectionDelay = OptionConverter.toInt(value, reconnectionDelay);
    }
  }

  /**
     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 );
	      }
      }
    }
  }
}
