package daruma.server;

import java.io.OutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.SocketException;
import java.util.List;
import java.util.ArrayList;
import java.util.Date;

import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import daruma.global_switch.ImplementationSwitches;
import daruma.storage_manager.DBMSStorageManager;
import daruma.storage_manager.StorageException;
import daruma.auth.PermissionManager;
import daruma.auth.AuthenticationInfo;

import daruma.sql.DatabaseConnection;
import daruma.sql.DatabaseConnectionException;
import daruma.sql.PostgreSQLDatabaseConnectionFactory;
import daruma.sql.MySQLDatabaseConnectionFactory;
import daruma.wfs.SOAPWFSProcessor;
import daruma.wfs.SOAPFaultDocumentBuilder;
import daruma.util.EncodingConversionOutputStream;
import daruma.util.LogWriter;
import daruma.util.TeeInputStream;
import daruma.util.TeeOutputStream;
import daruma.util.FatalException;
import daruma.util.PropertyReader;

import daruma.xml.util.XMLFormatConverter;

import daruma.util.Itk;


public class ConnectionHandler implements Runnable
{
	private	static Object		lock = new Object();

	private DarumaDaemon		daemon;
	private InputStream		in;
	private OutputStream		out;
	private List<OutputStream>	outputStreamList
					 = new ArrayList<OutputStream>();
	private PermissionManager	permissionManager;
	private AuthenticationInfo	authInfo;
	private Socket			socket;

	private	DBMSStorageManager	storage;
	private	String			debugTag;
	private	boolean			debug = false;
	private	boolean			ioLog = false;
	private	File			ioLogDirectory = null;
	private	boolean			compressReceiveStreamByGzip = false;
	private	boolean			compressSendStreamByGzip = false;
	private	long			connectionID = -1;
	private	boolean			isShuttingDown = false;

	private	String			databaseName = null;
	private	String			user = null;
	private	String			passwd = null;

	public	ConnectionHandler( DarumaDaemon daemon,
				   Socket socket,
				   InputStream in,
				   OutputStream out,
				   AuthenticationInfo authInfo,
				   String databaseName,
				   String user,
				   String passwd,
				   boolean compressReceiveStreamByGzip,
				   boolean compressSendStreamByGzip,
				   String debugTag,
				   boolean debug,
				   boolean ioLog,
				   File ioLogDirectory,
				   long connectionID )
					throws DatabaseConnectionException,
					       IOException
	{
		this.daemon = daemon;
		this.socket = socket;
		this.in = in;
		this.out = out;
		this.outputStreamList.add( this.out );
		this.authInfo = authInfo;

		this.storage = null;
		this.debugTag = debugTag;
		this.debug = debug;
		this.ioLog = ioLog;
		this.ioLogDirectory = ioLogDirectory;
		this.connectionID = connectionID;
		this.compressReceiveStreamByGzip = compressReceiveStreamByGzip;
		this.compressSendStreamByGzip = compressSendStreamByGzip;
		this.isShuttingDown = false;
		this.permissionManager = null;

		this.databaseName = databaseName;
		this.user = user;
		this.passwd = passwd;
	}

	public	void	run()
	{
		//
		// register socket information
		//
		if ( this.daemon != null )
		{
		    this.daemon.getConnectionInfoHolder()
			    .addClientSocket( this.socket, this.connectionID,
					      new Date() );
		}


		//
		// exclusive control for processing by single thread
		//
		synchronized( ConnectionHandler.lock )
		{
		    if ( this.daemon != null )
		    {
			this.daemon.getConnectionInfoHolder()
			    .setCurrentClientSocket( this.connectionID );
		    }

		    try
		    {
			this.logOpenSocket();
			this.runImplement();
		    }
		    catch( DatabaseConnectionException e )
		    {
			e.printStackTrace();
		    }
		    catch( IOException  e )
		    {
			e.printStackTrace();
		    }
		    finally
		    {
			this.logCloseSocket();
		    }
		}
	}


	protected void logOpenSocket()
	{
	    LogWriter.qwrite( "INFO",
			      "[" + Itk.getCurrentTimeStr() + "] ",
			      "Open  :", this.debugTag );
	}

	protected void logCloseSocket()
	{
	    LogWriter.qwrite( "INFO",
			      "[" + Itk.getCurrentTimeStr() + "] ",
			      "Close :", this.debugTag );
	}


	public	void	runImplement() throws DatabaseConnectionException,
					      IOException
	{
		//
		// prepare strage
		//
		DatabaseConnection backendDB;

		if ( ImplementationSwitches.instance().isPostGISBackend() )
		{
		    backendDB = new PostgreSQLDatabaseConnectionFactory()
								    .create();
		}
		else if ( ImplementationSwitches.instance().isMySQLBackend() )
		{
		    backendDB = new MySQLDatabaseConnectionFactory
			        ( ImplementationSwitches.instance()
				  .getMySQLHost(),
				  ImplementationSwitches.instance()
				  .getMySQLPort() ).create();
		}
		else
		{
		    throw new DatabaseConnectionException
			      ( "Unknown backend DBMS" );
		}

		this.storage = new DBMSStorageManager( backendDB );

		try
		{
		    this.storage.connect( databaseName , user , passwd );
		}
		catch( DatabaseConnectionException e )
		{
		    throw e;
		}


		boolean isRreadOnly = PropertyReader.getProperty
				      ( "daruma.run_mode" , "normal")
				      .equals( "read-only" );

		try
		{
		    this.storage.setReadOnly( isRreadOnly );
		}
		catch( StorageException e )
		{
		    throw new DatabaseConnectionException( e );
		}


		//
		// prepare permission manager
		//
		try
		{
		    this.permissionManager
			= this.storage.restorePermissionManager
			  ( ImplementationSwitches.instance()
			    .getAuthDefaultRule() );
		}
		catch( StorageException e )
		{
		    throw new DatabaseConnectionException( e );
		}


		try
		{
			//
			// prepare I/O
			//
			this.in  = new BufferedInputStream( this.in );
			this.out = get_ostream( new BufferedOutputStream
						( this.out ) );

			if ( this.compressSendStreamByGzip )
			{
				this.out = new GZIPOutputStream( this.out );
				this.outputStreamList.add( this.out );
			}

			if ( this.compressReceiveStreamByGzip )
			{
				this.in  = new GZIPInputStream( this.in );
			}

			if ( this.debug )
			{
				this.in  = new TeeInputStream
					       ( this.in  ,
						 System.out , false );

				this.out = new TeeOutputStream
					       ( this.out ,
						 System.out , false );
				this.outputStreamList.add( this.out );
			}

			if ( this.ioLog )
			{
				String	fileNameBase
					= "log-" + this.connectionID;

				OutputStream	inputLogFile;
				inputLogFile = new FileOutputStream
						( new File
						  ( this.ioLogDirectory ,
						    fileNameBase
						    + "-from-client" ) );

				OutputStream	outputLogFile;
				outputLogFile = new FileOutputStream
						( new File
						  ( this.ioLogDirectory ,
						    fileNameBase
						    + "-to-client" ) );

				this.in  = new TeeInputStream
					       ( this.in  ,
						 inputLogFile , false );

				this.out = new TeeOutputStream
					       ( this.out ,
						 outputLogFile , false );
				this.outputStreamList.add( this.out );
			}


			//
			// process document
			//
			SOAPWFSProcessor	p = new SOAPWFSProcessor();

			p.setDebugTag( this.debugTag );


			final int timeout = ImplementationSwitches.instance().getNetTimeoutMSec();
			if ( timeout > 0
			     && this.socket != null )
			{
			    final int timeoutSec = timeout / 1000;
			    final int timeoutMSec = timeout % 1000;

			    if ( this.debug )
			    {
				LogWriter.qwrite
				    ( "DEBUG",
				      "[" + Itk.getCurrentTimeStr() + "] ",
				      "set timeout = ", timeout, " msec",
				      " (= ", timeoutSec, " sec, ", timeoutMSec,
				      " msec)",
				      ": ", this.debugTag );
			    }

			    this.socket.setSoTimeout( timeout );
			}


			p.process( this.in, this.out, this.authInfo, storage, this.permissionManager );

			//
			// close database connection
			//
			try
			{
				this.storage.close();
				this.storage = null;
			}
			catch( DatabaseConnectionException  e )
			{
				throw e;
			}
		}
		catch( SocketException  e )
		{
		    // may be closed by client or shutdown hook of this class
		    if ( ! this.isShuttingDown )
		    {
			e.printStackTrace();
		    }
		}
		catch( IOException  e )
		{
			try
			{
				XMLFormatConverter
				    .print( (new SOAPFaultDocumentBuilder( e ))
					    .newDocument() ,
					    this.out );
			}
			catch( ParserConfigurationException pe )
			{
				pe.printStackTrace();
			}
			catch( TransformerException te )
			{
				te.printStackTrace();
			}
		}
		catch( DatabaseConnectionException dce )
		{
			dce.printStackTrace();
		}
		catch( FatalException  e )
		{
			e.printStackTrace();
		}
		finally
		{
			//
			// close I/O connection
			//
			try
			{
				this.closeAllOutputStream();
			}
			catch( IOException  e )
			{
				e.printStackTrace();
			}

			try
			{
				this.closeAllInputStream();
			}
			catch( IOException  e )
			{
				e.printStackTrace();
			}

			try
			{
				this.closeSocket();
			}
			catch( IOException  e )
			{
				e.printStackTrace();
			}
		}
	}

	private void closeAllInputStream() throws IOException
	{
	    if ( this.in != null )
	    {
		this.in.close();
		this.in = null;
	    }
	}

	private void closeAllOutputStream() throws IOException
	{
	    LogWriter.qwrite( "DEBUG",
			      "[" + Itk.getCurrentTimeStr() + "] ",
			      this.getClass().getName(),
			      ".", "closeAllOutputStream(), ", this.debugTag );

	    for ( int i = this.outputStreamList.size() - 1 ; i >= 0 ; -- i )
	    {
		OutputStream ostr = this.outputStreamList.get(i);
		LogWriter.qwrite( "DEBUG",
				  "[" + Itk.getCurrentTimeStr() + "] ",
				  "flush OutputStream[", i, "]: ", ostr, ", ",
				  this.debugTag );

		ostr.flush();

		if ( ostr instanceof GZIPOutputStream )
		{
		    ((GZIPOutputStream)ostr).finish();
		}
	    }

	    for ( int i = this.outputStreamList.size() - 1 ; i >= 0 ; -- i )
	    {
		OutputStream ostr = this.outputStreamList.get(i);
		LogWriter.qwrite( "DEBUG",
				  "[" + Itk.getCurrentTimeStr() + "] ",
				  "close OutputStream[", i, "]: ", ostr, ", ",
				  this.debugTag );
		ostr.close();
	    }

	    this.outputStreamList.clear();
	    this.out = null;
	}

	private void closeSocket() throws IOException
	{
	    if ( this.daemon != null )
	    {
		daemon.getConnectionInfoHolder().closeSocket( this.connectionID );
	    }

	    this.socket = null;
	}


	private static Boolean cEncodingWarned = false;

	/** XXX:
	 *
	 * InputStream/OutputStream are now obsolete for text I/O.
	 * Use Reader/Writer for these purpose.
	 *
	 * These lines are some trivial fix.
	 * We must fix it.
	 */
	private OutputStream get_ostream(OutputStream raw) {
		String       encoding = System.getProperty("file.encoding");
		OutputStream enc;

		if ( ! (encoding.equalsIgnoreCase("utf-8") ||
			encoding.equalsIgnoreCase("utf8"))) {
			if (!cEncodingWarned) {
				LogWriter.qwrite("WARN", "I/O encoding is not UTF-8. Applies trivial encoding conversion.");
				cEncodingWarned = true;
			}
			try {
				enc = new EncodingConversionOutputStream(raw, "utf-8");
				this.outputStreamList.add( enc );
			} catch(UnsupportedEncodingException uee) {
				LogWriter.qwrite("FATAL", "Encoding name error. Message: ", uee.getMessage());
				enc = null;

				throw new RuntimeException();
			}
		} else {
			enc = raw;
		}

		return enc;
	}
}
