package daruma.server;

import daruma.server.ConnectionHandler;

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.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.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 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;

	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.debugTag = debugTag;
		this.debug = debug;
		this.ioLog = ioLog;
		this.ioLogDirectory = ioLogDirectory;
		this.connectionID = connectionID;
		this.compressReceiveStreamByGzip = compressReceiveStreamByGzip;
		this.compressSendStreamByGzip = compressSendStreamByGzip;

		DatabaseConnection backendDB;

		if ( ImplementationSwitches.instance().isPostGISBackend() )
		{
		    backendDB = new PostgreSQLDatabaseConnectionFactory()
								    .create();
		}
		else if ( ImplementationSwitches.instance().isMySQLBackend() )
		{
		    backendDB = new MySQLDatabaseConnectionFactory().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 );
		}
	}


	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
		    {
			LogWriter.qwrite("INFO",
					 "[" + Itk.getCurrentTimeStr() + "] ",
					 "Open  :", this.debugTag) ;
			this.runImplement();
		    } 
		    finally
		    {
			LogWriter.qwrite("INFO",
					 "[" + Itk.getCurrentTimeStr() + "] ",
					 "Close :", this.debugTag) ;
		    }
		}
	}

	public	void	runImplement()
	{
		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 );

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

			//
			// close database connection
			//
			try
			{
				this.storage.close();
			}
			catch( DatabaseConnectionException  e )
			{
				throw e;
			}


			//
			// close I/O connection
			//
			this.closeAllOutputStream();
			this.in.close();
		}
		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
		{
			try
			{
				this.closeAllOutputStream();
			}
			catch( IOException  e )
			{
				e.printStackTrace();
			}

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

			try
			{
				if ( this.daemon != null )
				{
					daemon.getConnectionInfoHolder()
					  .closeSocket( this.connectionID );
				}
			}
			catch( IOException  e )
			{
				e.printStackTrace();
			}
		}
	}

	private void closeAllOutputStream() throws IOException
	{
	    for ( int i = this.outputStreamList.size() -1 ; i >= 0 ; -- i )
	    {
		OutputStream ostr = this.outputStreamList.get(i);

		ostr.flush();

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

	    for ( int i = this.outputStreamList.size() -1 ; i >= 0 ; -- i )
	    {
		this.outputStreamList.get(i).close();
	    }
	}


	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;
	}
}
