package daruma.server;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.PrintWriter;
import java.util.Map;
import java.util.HashMap;
import java.util.TreeMap;

import javax.xml.transform.TransformerException;
import javax.xml.parsers.ParserConfigurationException;

import daruma.global_switch.ImplementationSwitches;
import daruma.sql.DatabaseConnectionException;
import daruma.util.LogWriter;
import daruma.util.PropertyReader;
import daruma.util.FatalException;
import daruma.xml.util.XMLFormatConverter;
import daruma.wfs.SOAPFaultDocumentBuilder;

import daruma.xml.util.XPointerUtil;
import daruma.util.ParseException;

import daruma.xml.URI;
/*
import daruma.xml.util.DOMDocumentFactory;
import org.w3c.dom.Document;
*/
import daruma.auth.AuthenticationInfo;
import daruma.auth.PermissionManager;

public class DarumaServlet extends HttpServlet
{
    private static final long serialVersionUID = 0L;

    private long connectionID = 0;

    private static boolean initialized = false;
    private synchronized void initializeEnvironment() throws FatalException
    {
	//
	// initialize global classes
	//
	if ( ! initialized )
	{
	    daruma.util.PropertyReader.initialize
		( this.getClass().getClassLoader().getResourceAsStream
		  ( daruma.util.PropertyReader.getDefaultPropertyFile() ) );
	    daruma.util.LogWriter.initialize
		( this.getClass().getClassLoader().getResource
		  ( daruma.util.PropertyReader.getDefaultPropertyFile() ) );

	    ImplementationSwitches.createInstance();

	    this.initialized = true;
	}
    }


    @Override
    public void doGet( HttpServletRequest request,
		       HttpServletResponse response )
	throws IOException
    {
	try
	{
	    this.initializeEnvironment();
	}
	catch( FatalException e )
	{
	    throw new IOException( "configuration error: " + e.getMessage() );
	}


	daruma.util.LogWriter.qwrite("INFO",
				     "query = ["
				     + request.getQueryString() + "]" );

	response.setContentType( "text/xml; charset=UTF-8" );


	AuthenticationInfo authInfo = this.getAuthenticatedInfo( request );

	LogWriter.qwrite("INFO",
			 "auth = ",
			 authInfo.certificated(), ", ",
			 "user = [",
			 authInfo.getAuthenticatedAccount().getName(), "]" );


	//
	// wsdl
	//
	if ( request.getQueryString() != null
	     && request.getQueryString().equals( "wsdl" ) )
	{
	    response.getOutputStream().print( this.getWSDL() );

	    return;
	}


	//
	// get parameters
	//
	final String service = this.getParameter( request, "SERVICE" );
	final String version = this.getParameter( request, "VERSION" );
	final String req     = this.getParameter( request, "REQUEST" );
	final String ns      = this.getParameter( request, "NAMESPACE" );


	//
	// check each parameters
	//

	// check SERVICE
	if ( service == null )
	{
	    this.returnError( "no SERVICE parameter found."
			      + " add SERVICE parameter.\n"
			      + " format: SERVICE=WFS",
			      response );

	    return;
	}
	else if ( ! service.equals( "WFS" )
	       && ! service.equals( "MISP" ) )
	{
	    this.returnError( "SERVICE type [" + service + "] unsupported, "
			      + "expected was WFS or MISP",
			      response );

	    return;
	}

	// check VERSION
	if ( version == null )
	{
	    if ( ! ( req != null && req.equals( "GetCapabilities" ) ) )
	    {
		this.returnError( "no VERSION parameter found."
				  + " add VERSION parameter.\n"
				  + " format: VERSION=1.1.0",
				  response );

		return;
	    }
	}
	else if ( ! version.equals( "1.1.0" )
	       && ! version.equals( "1.0.0" ) )
	{
	    this.returnError( "VERSION [" + version + "] not supported,"
			      + " use VERSION=1.1.0 or 1.0.0",
			      response );

	    return;
	}

	// check NAMESPACE
	final String namespaceParam = this.getParameter( request, "NAMESPACE" );

	Map<String, String> namespaceMap;
	if ( namespaceParam == null )
	{
	    namespaceMap = new TreeMap<String, String>();
	}
	else
	{
	    try
	    {
		namespaceMap = XPointerUtil.parseXmlnsString( namespaceParam );
	    }
	    catch( ParseException e )
	    {
		this.returnError( e.getMessage(), response );

		return;
	    }
	}

	if ( req == null )
	{
	    // XXX: return more detail message, REQUEST candidates
	    this.returnError( "no REQUEST found. add REQUEST parameter.",
			      response );

	    return;
	}


	//
	// handle each operation
	//
	String reqStr = null;

	try
	{
	    if ( req.equals( "GetCapabilities" ) )
	    {
		reqStr = "<_misp:GetCapabilities"
			 + " xmlns:_misp=\"http://www.infosharp.org/misp\"/>";
	    }
	    else if ( req.equals( "DescribeFeatureType" ) )
	    {
		reqStr = this.getDescribeFeatureTypeRequest( request,
							     namespaceMap );
	    }
	    else if ( req.equals( "GetFeature" ) )
	    {
		reqStr = this.getGetFeatureRequest( request, namespaceMap );
	    }
	    else if ( req.equals( "Transaction" )
		   || req.equals( "RegisterFeatureType" )
		   || req.equals( "RegisterCoordinateSystemTransformation" ) )
	    {
		this.returnError( "REQUEST [" + req + "] by http GET method"
				  + " not supported."
				  + " Use http POST instead.",
				  response );

		return;
	    }
	    else
	    {
		// XXX: return more detail message, such as REQUEST candidates
		this.returnError( "bad REQUEST [" + req + "]", response );

		return;
	    }

	    System.err.println( reqStr );

	    //
	    // delegate to main handler
	    //
	    this.handleConnection
		 ( new ByteArrayInputStream( reqStr.getBytes() ),
		   response.getOutputStream(),
		   authInfo );
	}
	catch( IOException e )
	{
	    this.returnError( e.getMessage(), response );

	    return;
	}
    }


    @Override
    public void doPost( HttpServletRequest request,
			HttpServletResponse response )
	throws IOException
    {
	try
	{
	    this.initializeEnvironment();
	}
	catch( FatalException e )
	{
	    throw new IOException( "configuration error: " + e.getMessage() );
	}

	response.setContentType( "text/xml; charset=UTF-8" );

	this.handleConnection( request.getInputStream(),
			       response.getOutputStream(),
			       this.getAuthenticatedInfo( request ) );
    }


    private AuthenticationInfo getAuthenticatedInfo
				( HttpServletRequest request )
    {
	final String authName = request.getRemoteUser();

	daruma.util.LogWriter.qwrite("DEBUG",
				     "AuthType = ["
				     + request.getAuthType() + "]" );
	daruma.util.LogWriter.qwrite("DEBUG",
				     "UserPrincipal = ["
				     + request.getUserPrincipal() + "]" );
	daruma.util.LogWriter.qwrite("DEBUG",
				     "RemoteUser = [" + authName + "]" );

	if ( authName != null )
	{
	    daruma.util.LogWriter.qwrite("INFO",
					 "authenticated name = ["
					 + authName + "]" );
	}
	else
	{
	    daruma.util.LogWriter.qwrite("INFO",
					 "not authenticated" );
	}

	return new AuthenticationInfo( authName );
    }


    private void handleConnection( InputStream in, OutputStream out,
				   AuthenticationInfo authInfo )
							throws IOException
    {
	//
	// create connection
	//
	ConnectionHandler c;

	try
	{
	    c = new ConnectionHandler
		    ( null,
		      null,
		      in,
		      out,
		      authInfo,
		      PropertyReader.getProperty( "daruma.db.name" ),
		      PropertyReader.getProperty( "daruma.db.uid" ),
		      PropertyReader.getProperty( "daruma.db.pass" ),
		      false, // compress receiving stream by gzip
		      false, // compress sending stream by gzip
		      "servlet",
		      true,
		      false,
		      null,
		      this.connectionID );

	    this.connectionID ++;
	}
	catch( DatabaseConnectionException  e )
	{
	    throw new IOException( e.getMessage() );
	}


	//
	// do main process
	//
	c.run();


	//
	// release connection
	//
	c = null;


	//
	// memory debug
	//
	if ( ImplementationSwitches.instance().getDoFullGC() )
	{
	    Runtime.getRuntime().gc();
	}
    }


    private void returnError( String  errorMessage,
			      HttpServletResponse  response )
						throws IOException
    {
	try
	{
	    XMLFormatConverter.print( new SOAPFaultDocumentBuilder
					  ( errorMessage ).newDocument(),
				      response.getOutputStream() );
	}
	catch( ParserConfigurationException e )
	{
	    e.printStackTrace();
	}
	catch( TransformerException e )
	{
	    e.printStackTrace();
	}
    }

    private String getDescribeFeatureTypeRequest
			( HttpServletRequest request,
			  Map<String, String> namespaceMap )
	throws IOException
    {
	String typeNameParam = this.getParameter( request, "TYPENAME" );

	if ( typeNameParam == null )
	{
	    throw new IOException( "no TYPENAME parameter found."
				   + " add TYPENAME parameter." );
	}

	/*
	Document doc = DOMDocumentFactory.create();

	Element describeFeatureTypeElement
		    = doc.createElementNS( URI.MISP,
					   "DescribeFeatureType" );

	doc.appendChild( describeFeatureTypeElement );

	Element typeNameElement
		    = doc.createElementNS( URI.MISP, "TypeName" );
	describeFeatureTypeElement.appendChild( typeNameElement );

	Text type = doc.createText();
	*/

	StringBuilder buf = new StringBuilder();

	buf.append( "<_misp:DescribeFeatureType"
		    + " xmlns:_misp=\"" + URI.MISP + "\"" );

	for( Map.Entry<String, String> e : namespaceMap.entrySet() )
	{
	    buf.append( " " );
	    if ( e.getKey() == null )
	    {
		buf.append( "xmlns=\"" );
		buf.append( e.getValue() );
		buf.append( "\"" );
	    }
	    else
	    {
		buf.append( "xmlns:" );
		buf.append( e.getKey() );
		buf.append( "=\"" );
		buf.append( e.getValue() );
		buf.append( "\"" );
	    }
	}

	buf.append( ">\n" );
	buf.append( " " );
	buf.append( "<_misp:TypeName>");
	buf.append( typeNameParam );
	buf.append( "</_misp:TypeName>");
	buf.append( "</_misp:DescribeFeatureType>" );

	return buf.toString();
    }


    private String getGetFeatureRequest( HttpServletRequest request,
					 Map<String, String> namespaceMap )
	throws IOException
    {
	final String typeNameParam  = this.getParameter( request, "TYPENAME" );

	if ( typeNameParam == null )
	{
	    throw new IOException( "no TYPENAME parameter found."
				   + " add TYPENAME parameter." );
	}

	final String bbox = this.getParameter( request, "BBOX" );
	final String filter = this.getParameter( request, "FILTER" );

	if ( bbox != null && filter != null )
	{
	    throw new IOException( "BBOX and FILTER is exclusive." );
	}

	if ( bbox != null )
	{
	    throw new IOException( "BBOX parameter is not supported yet." );
	}


	StringBuilder buf = new StringBuilder();

	buf.append( "<_misp:GetFeature xmlns:_misp=\"" + URI.MISP + "\"" );
	buf.append( " xmlns:_gml=\"" + URI.GML + "\"" );

	for( Map.Entry<String, String> e : namespaceMap.entrySet() )
	{
	    buf.append( " " );
	    if ( e.getKey() == null )
	    {
		buf.append( "xmlns=\"" );
		buf.append( e.getValue() );
		buf.append( "\"" );
	    }
	    else
	    {
		buf.append( "xmlns:" );
		buf.append( e.getKey() );
		buf.append( "=\"" );
		buf.append( e.getValue() );
		buf.append( "\"" );
	    }
	}

	buf.append( ">\n" );
	buf.append( " <_misp:Query typeName=\"" );
	buf.append( typeNameParam );
	buf.append( "\">\n" );
	if ( filter != null )
	{
	    buf.append( filter );
	}
	else if ( bbox != null )
	{
	    String[] values = bbox.split( "," );

	    if ( values.length != 4 )
	    {
		throw new IOException( "invalid BBOX parameter." );
	    }

	    //
	    // XXX: not implemented yet
	    //
	}
	else
	{
	    buf.append( "  <_misp:Filter>\n" );
	    buf.append( "   <_misp:True/>\n" );
	    buf.append( "  </_misp:Filter>\n" );
	}
	buf.append( " </_misp:Query>\n" );
	buf.append( "</_misp:GetFeature>" );

	daruma.util.LogWriter.qwrite( "INFO",
				      "wfs query = [" + buf.toString() + "]" );

	return buf.toString();
    }

    private String getWSDL() throws IOException
    {
	StringBuilder buf = new StringBuilder();

	buf.append( "<wsdl:definition"
		    + " xmlns:wsdl=\"" + "http://schemas.xmlsoap.org/wsdl/"
		    + "\">" );

	buf.append( "</wsdl:definition>" );

	return buf.toString();
    }

    private String getParameter( HttpServletRequest request, String paramName )
    {
	String value = request.getParameter( paramName );

	if ( value == null )
	{
	    value = request.getParameter( paramName.toLowerCase() );
	}

	return value;
    }
}
