package daruma.server;

import daruma.server.ConnectionHandler;
import daruma.server.DarumaDaemonAdministrator;
import daruma.server.ConnectionInfoHolder;
import daruma.server.DarumaVersion;

import daruma.global_switch.ImplementationSwitches;
import daruma.util.PortNumberValidator;
import daruma.util.LogWriter;
import daruma.util.PropertyReader;
import daruma.util.FatalException;
import daruma.util.Itk;
import daruma.sql.DatabaseConnectionException;
import daruma.sql.PostgreSQLDatabaseConnectionFactory;
import daruma.sql.MySQLDatabaseConnectionFactory;
import daruma.auth.AuthenticationInfo;

import java.net.*;
import java.io.*;

public class DarumaDaemon
{
	static	final	public	int	DEFAULT_PORT = PropertyReader.getProperty("daruma.serv.port", 5050);
	static	final	public	int	DEFAULT_ADMIN_PORT = PropertyReader.getProperty("daruma.serv.admin_port", 5051);
	static	final	public	boolean	DEFAULT_USE_ADMIN_PORT = PropertyReader.getProperty("daruma.serv.use_admin_port", true);
	static	final	public	boolean	ADMIN_PORT_PRINT_USAGE = PropertyReader.getProperty("daruma.serv.admin_port_print_usage", false);

	static	final	public	String	DEFAULT_DATABASE_NAME = "daruma$devel";
	static	final	public	String	DEFAULT_USER = PropertyReader.getProperty("daruma.db.uid", "daruma");
	static	final	public	String	DEFAULT_PASSWORD = PropertyReader.getProperty("daruma.db.pass", "");
	static	final	public	String	DEFAULT_CONNECTION_LOG_DIRECTORY = ".";

	private	int	port;
	private	String	databaseName;
	private	String	user;
	private	String	password;
	private	boolean	debug;
	private	boolean	useGzipForReceiving;
	private	boolean	useGzipForSending;
	private	boolean	connectionLog;
	private	File	connectionLogDirectory;
	private	boolean	useAdminPort;
	private	int	adminPort;

	private	DarumaDaemonAdministrator admin;
	private ServerSocket socket;
	private	boolean	isShuttingDown;

	private	long	connectionID;

	private ConnectionInfoHolder connectionInfoHandler = new ConnectionInfoHolder();
	public ConnectionInfoHolder getConnectionInfoHolder()
	{
	    return this.connectionInfoHandler;
	}


	public	DarumaDaemon( int  port ,
			      String  databaseName ,
			      String  user ,
			      String  password,
			      boolean useGzipForReceiving,
			      boolean useGzipForSending,
			      boolean useAdminPort,
			      int adminPort )
	{
		this.port = port;
		this.debug = false;
		this.useGzipForReceiving = useGzipForReceiving;
		this.useGzipForSending = useGzipForSending;
		this.connectionLog = false;
		this.connectionLogDirectory = null;
		this.databaseName = databaseName;
		this.user = user;
		this.password = password;
		this.useAdminPort = useAdminPort;
		this.adminPort = adminPort;

		this.socket = null;
		this.isShuttingDown = false;
		this.connectionID = 0;
	}

	public	boolean	run()
	{
		try
		{
			if ( ! this.checkConfig() )
			{
			    return false;
			}


			this.socket = new ServerSocket( port );
			this.socket.setReuseAddress( true );

			if ( this.useAdminPort )
			{
			    this.admin = new DarumaDaemonAdministrator
					     ( this, this.adminPort,
					       ADMIN_PORT_PRINT_USAGE );

			    (new Thread( this.admin )).start();
			}

			LogWriter.qwrite("INFO",
					 "[" + Itk.getCurrentTimeStr() + "] ",
					 "Listen:", socket) ;

			Runtime.getRuntime().addShutdownHook
			    ( new Thread()
				{
				    public void run()
				    {
					admin.shutdown();
					shutdown();
				    }
				} );

			for(;;)
			{
				if ( ImplementationSwitches.instance().getDoFullGC() )
				{
				    Runtime.getRuntime().gc();
				}


				if ( this.isShuttingDown )
				{
					break;
				}

				Socket s = null;
				try
				{
					s = this.socket.accept();
				}
				catch( SocketException e )
				{
					if ( ! this.isShuttingDown )
					{
						throw e;
					}
				}

				if ( this.isShuttingDown )
				{
					if ( s != null )
					{
						try
						{
						    s.close();
						}
						catch( IOException e )
						{
						    e.printStackTrace();
						}
					}

					break;
				}


				/*
				// doesn't work on Windows Vista
				s.setKeepAlive( true );
				*/

				/*
				// no effect?
				s.setSendBufferSize( 1024 * 1024 );
				s.setReceiveBufferSize( 1024 * 1024 );
				*/

				ConnectionHandler c;

				try
				{
					c = new ConnectionHandler
						( this,
						  s,
						  s.getInputStream(),
						  s.getOutputStream(),
						  new AuthenticationInfo(),
						  this.databaseName,
						  this.user,
						  this.password,
						  this.useGzipForReceiving,
						  this.useGzipForSending,
						  s.toString(),
						  this.debug,
						  this.connectionLog,
						  this.connectionLogDirectory,
						  this.connectionID );

					this.connectionID ++;
				}
				catch( DatabaseConnectionException  e )
				{
					LogWriter.qwrite( "ERROR", e.getMessage() );
					continue;
				}
				catch( SocketException e )
				{
					LogWriter.qwrite( "INFO", e.getMessage() );
					e.printStackTrace();

					continue;
				}

				Thread th = new Thread( c );
				th.start();
			}
		}
		catch( java.net.BindException  e )
		{
			LogWriter.qwrite("ERROR",
					 "Can't bind server socket: ",
					 e.getMessage() );

			System.err.println( "Can't bind server socket: "
					    + e.getMessage() );
			return( false );
		}
		catch( IOException  e )
		{
			e.printStackTrace();
			return( false );
		}
		finally
		{
			if ( this.socket != null )
			{
				try
				{
					LogWriter.qwrite
					    ("INFO",
					     "[" + Itk.getCurrentTimeStr() + "] ",
					     "Close:", socket);

					this.socket.close();
				}
				catch( IOException  e )
				{
					e.printStackTrace();

					return( false );
				}
			}
		}

		return( true );
	}

	private boolean checkConfig()
	{
	    if ( ! PortNumberValidator.check( this.port ) )
	    {
		LogWriter.qwrite( "ERROR",
				  "configuration error: ",
				  "invalid port number: ",
				  port );

		System.err.println( "invalid port number: " + port );
		return false;
	    }

	    if ( ! PortNumberValidator.check( this.adminPort ) )
	    {
		LogWriter.qwrite( "ERROR",
				  "configuration error: ",
				  "invalid admin port number: ",
				  adminPort );

		System.err.println( "invalid admin port number: "
				    + adminPort );
		return false;
	    }

	    return true;
	}




	public void setDebug( boolean  debug )
	{
		this.debug = debug;
	}

	public void setConnectionLog( boolean  connectionLog ,
				      File  connectionLogDirectory )
	{
		this.connectionLog = connectionLog;
		this.connectionLogDirectory = connectionLogDirectory;
	}

	public void shutdown()
	{
		if ( this.isShuttingDown )
		{
		    return;
		}

		this.isShuttingDown = true;

		try
		{
			LogWriter.qwrite("INFO",
					 "[" + Itk.getCurrentTimeStr() + "] ",
					 "Close:", socket);

			this.socket.close();
			this.socket = null;
		}
		catch( IOException  e )
		{
			e.printStackTrace();
		}
	}

	public static void main( String  args[] ) throws FatalException
	{
		DarumaDaemonOptionAnalyzer	opt;
		opt = new DarumaDaemonOptionAnalyzer
			( args , "DarumaDaemon" ,
			  DarumaDaemon.DEFAULT_PORT ,
			  DarumaDaemon.DEFAULT_DATABASE_NAME ,
			  DarumaDaemon.DEFAULT_USER ,
			  DarumaDaemon.DEFAULT_PASSWORD ,
			  DarumaDaemon.DEFAULT_CONNECTION_LOG_DIRECTORY ,
			  DarumaDaemon.DEFAULT_USE_ADMIN_PORT,
			  DarumaDaemon.DEFAULT_ADMIN_PORT );

		if ( ! opt.analyze() )
		{
			opt.printHelp( System.err );

			System.exit( 1 );
		}
		else if ( opt.getHelp() )
		{
			opt.printHelp( System.out );

			System.exit( 0 );
		}
		else if ( opt.getVersion() )
		{
			System.out.println( DarumaVersion.getVersion() );
			System.exit( 0 );
		}

		if ( opt.getDebug() )
		{
			LogWriter.setVerbose();
		}


		ImplementationSwitches.createInstance();

		File	connectionLogDirectory
			  = new File( opt.getConnectionLogDirectory() );

		if ( opt.getConnectionLog() )
		{
			if ( connectionLogDirectory.exists() )
			{
				if ( ! connectionLogDirectory.isDirectory() )
				{
					String	err;
					err = "connection log directory ["
						+ connectionLogDirectory
						+ "] is not a directory";

					System.err.println( err );
					LogWriter.qwrite( "FATAL" , err );

					System.exit( 1 );
				}
			}
			else
			{
				if ( ! connectionLogDirectory.mkdirs() )
				{
					String	err;
					err = "Can't create"
						+ " connection log directory ["
						+ connectionLogDirectory
						+ "]";

					System.err.println( err );
					LogWriter.qwrite( "FATAL" , err );

					System.exit( 1 );
				}
			}
		}

		try {
		    if ( ImplementationSwitches.instance()
			 .isPostGISBackend() )
		    {
			new PostgreSQLDatabaseConnectionFactory().loadClass();
		    }
		    else if ( ImplementationSwitches.instance()
			      .isMySQLBackend() )
		    {
			new MySQLDatabaseConnectionFactory
			    ( ImplementationSwitches.instance()
			      .getMySQLHost(),
			      ImplementationSwitches.instance()
			      .getMySQLPort() ).create();
		    }
		    else
		    {
			throw new RuntimeException( "Unknown backend DBMS" );
		    }
		} catch( DatabaseConnectionException
			 .ClassLoadFailedDatabaseConnectionException  e ) {
			LogWriter.qwrite("FATAL",  "Can't load class: "
					 + e.getFailedClassName() );

			throw new RuntimeException("Can't load class: "
						   + e.getFailedClassName());
		} catch(DatabaseConnectionException dce) {
			LogWriter.qwrite("FATAL", "Can't load DB class");
			throw new RuntimeException("Can't load DB class: "
						   + dce.getMessage());
		}


		DarumaDaemon daemon = new DarumaDaemon
					  ( opt.getPortNumber() ,
					    opt.getDatabaseName() ,
					    opt.getUser() ,
					    opt.getPassword(),
					    opt.getGzipReceive(),
					    opt.getGzipSend(),
					    opt.getUseAdminPort(),
					    opt.getAdminPortNumber() );

		daemon.setDebug( opt.getDebug() );
		daemon.setConnectionLog( opt.getConnectionLog() ,
					 connectionLogDirectory );

		if ( ! daemon.run() ) {
			System.exit( 1 );
		}
	}


	public static class  DarumaDaemonOptionAnalyzer
	{
		private	String[]	args;
		private	String		programName;
		private	int		portNumber;
		private	String		databaseName;
		private	String		user;
		private	String		password;
		private	boolean		useAdminPort;
		private	int		adminPortNumber;

		private	boolean	helpFlag = false;
		private	boolean	versionFlag = false;
		private	boolean	debugFlag = false;
		private	boolean	connectionLogFlag = false;
		private	String	connectionLogDirectory = null;
		private boolean gzipReceiveFlag = false;
		private boolean gzipSendFlag = false;

		public	DarumaDaemonOptionAnalyzer
				( final String[]  args ,
				  final String  programName ,
				  final int     defaultPortNumber ,
				  final String  defaultDatabaseName ,
				  final String  defaultUser ,
				  final String  defaultPassword ,
				  final String  defaultConnectionLogDirectory,
				  final boolean defaultUseAdminPort,
				  final int     defaultAdminPortNumber )
		{
			this.args = args.clone();
			this.programName = programName;
			this.portNumber = defaultPortNumber;
			this.databaseName = defaultDatabaseName;
			this.user = defaultUser;
			this.password = defaultPassword;
			this.useAdminPort = defaultUseAdminPort;
			this.adminPortNumber = defaultAdminPortNumber;
			this.connectionLogFlag = false;
			this.connectionLogDirectory
				= defaultConnectionLogDirectory;
		}

		public	boolean	analyze()
		{
			int	index = 0;
			while( index < args.length )
			{
				final	String	arg = this.args[index];
				int	numArgs = args.length - index - 1;

				if ( arg.equals( "--help" ) )
				{
					this.helpFlag = true;
				}
				else if ( arg.equals( "--version" ) )
				{
					this.versionFlag = true;
				}
				else if ( arg.equals( "--port" ) )
				{
					if ( numArgs < 1 )
					{
						return( false );
					}

					this.portNumber
					    = this.parsePortNumber
					      ( this.args[index + 1] );

					if ( this.portNumber < 0 )
					{
					    return false;
					}

					index ++;
				}
				else if ( arg.equals( "--database" ) )
				{
					if ( numArgs < 1 )
					{
						return( false );
					}

					this.databaseName
						= this.args[index + 1];

					index ++;
				}
				else if ( arg.equals( "--user" ) )
				{
					if ( numArgs < 1 )
					{
						return( false );
					}

					this.user = this.args[index + 1];

					index ++;
				}
				else if ( arg.equals( "--password" ) )
				{
					if ( numArgs < 1 )
					{
						return( false );
					}

					this.password = this.args[index + 1];

					index ++;
				}
				else if ( arg.equals( "--debug" ) )
				{
					this.debugFlag = true;
				}
				else if ( arg.equals( "--gzip" ) )
				{
					this.gzipReceiveFlag = true;
					this.gzipSendFlag = true;
				}
				else if ( arg.equals( "--gzip-receive" ) )
				{
					this.gzipReceiveFlag = true;
				}
				else if ( arg.equals( "--gzip-send" ) )
				{
					this.gzipSendFlag = true;
				}
				else if ( arg.equals( "--connection-log" ) )
				{
					this.connectionLogFlag = true;
				}
				else if ( arg.equals
					  ( "--connection-log-directory" ) )
				{
					if ( numArgs < 1 )
					{
						return( false );
					}

					index ++;

					this.connectionLogDirectory
						= this.args[index];
				}
				else if ( arg.equals( "--use-admin-port" ) )
				{
					this.useAdminPort = true;
				}
				else if ( arg.equals( "--admin-port" ) )
				{
					if ( numArgs < 1 )
					{
						return( false );
					}

					this.adminPortNumber
					    = this.parsePortNumber
					      ( this.args[index + 1] );

					if ( this.adminPortNumber < 0 )
					{
					    return false;
					}

					index ++;
				}
				else
				{
					LogWriter.qwrite
						( "invalid option: " + arg );

					System.err.println
						( "invalid option: " + arg );

					return( false );
				}

				index ++;
			}

			return( true );
		}

		private	int parsePortNumber( String value )
		{
		    try
		    {
			return Integer.parseInt( value );
		    }
		    catch( NumberFormatException  e )
		    {
			LogWriter.qwrite( "ERROR",
					  "invalid port number: ", value );

			System.err.println( "invalid port number: " + value );

			return -1;
		    }
		}

		public	void	printHelp( OutputStream  out )
		{
			String	mes = "";
			mes += "Usage: "
				   + this.programName + " [options]" + "\n";
			mes += "Possible options are:"               + "\n";
			mes += "      --help"                        + "\n";
			mes += "      --version"                     + "\n";
			mes += "      --port PORT"                   + "\n";
			mes += "      --use-admin-port"              + "\n";
			mes += "      --admin-port PORT"             + "\n";
			mes += "      --database DATABASE_NAME"      + "\n";
			mes += "      --user USER_NAME"              + "\n";
			mes += "      --password PASSWORD"           + "\n";
			mes += "      --debug"                       + "\n";
			mes += "      --connection-log"              + "\n";
			mes += "      --connection-log-directory DIRECTORY"
								     + "\n";
			mes += "      --gzip"                        + "\n";
			mes += "      --gzip-receive"                + "\n";
			mes += "      --gzip-send"                   + "\n";

			PrintStream	p = new PrintStream( out );
			p.print( mes );
		}

		public	boolean	getHelp()
		{
			return( this.helpFlag );
		}

		public	boolean	getVersion()
		{
			return( this.versionFlag );
		}

		public	int	getPortNumber()
		{
			return( this.portNumber );
		}

		public	String	getDatabaseName()
		{
			return( this.databaseName );
		}

		public	String	getUser()
		{
			return( this.user );
		}

		public	String	getPassword()
		{
			return( this.password );
		}

		public	boolean	getDebug()
		{
			return( this.debugFlag );
		}

		public	boolean	getGzipReceive()
		{
			return( this.gzipReceiveFlag );
		}

		public	boolean	getGzipSend()
		{
			return( this.gzipSendFlag );
		}

		public	boolean	getConnectionLog()
		{
			return( this.connectionLogFlag );
		}

		public	String	getConnectionLogDirectory()
		{
			return( this.connectionLogDirectory );
		}

		public	boolean	getUseAdminPort()
		{
			return( this.useAdminPort );
		}

		public	int	getAdminPortNumber()
		{
			return( this.adminPortNumber );
		}
	}
}
