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.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 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,
				   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.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 )
		{
		    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 , 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
			{
				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;
	}
}
