/*
 * ====================================================================
 * ======== The Apache Software License, Version 1.1
 * ==================
 * ==========================================================
 * Copyright (C) 2002 The Apache Software Foundation. All rights
 * reserved. Redistribution and use in source and binary forms, with
 * or without modifica- tion, are permitted provided that the
 * following conditions are met: 1. Redistributions of source code
 * must retain the above copyright notice, this list of conditions and
 * the following disclaimer. 2. Redistributions in binary form must
 * reproduce the above copyright notice, this list of conditions and
 * the following disclaimer in the documentation and/or other
 * materials provided with the distribution. 3. The end-user
 * documentation included with the redistribution, if any, must
 * include the following acknowledgment: "This product includes
 * software developed by SuperBonBon Industries
 * (http://www.sbbi.net/)." Alternately, this acknowledgment may
 * appear in the software itself, if and wherever such third-party
 * acknowledgments normally appear. 4. The names "UPNPLib" and
 * "SuperBonBon Industries" must not be used to endorse or promote
 * products derived from this software without prior written
 * permission. For written permission, please contact info@sbbi.net.
 * 5. Products derived from this software may not be called
 * "SuperBonBon Industries", nor may "SBBI" appear in their name,
 * without prior written permission of SuperBonBon Industries. THIS
 * SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU- DING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE. This software consists of voluntary contributions made
 * by many individuals on behalf of SuperBonBon Industries. For more
 * information on SuperBonBon Industries, please see
 * <http://www.sbbi.net/>.
 */

package net.tomp2p.upnp;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;


import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Message object for an UPNP action, simply call setInputParameter()
 * to add the required action message params and then service() to
 * receive the ActionResponse built with the parsed UPNP device SOAP
 * xml response.
 * 
 * @author <a href="mailto:superbonbon@sbbi.net">SuperBonBon</a>
 * @version 1.0
 */
public class ActionMessage
{
	private Service service;

	private Action serviceAction;

	private List<InputParamContainer> inputParameters;

	/**
	 * Protected constuctor so that only messages factories can build
	 * it
	 * 
	 * @param service
	 *           the service for which the
	 * @param serviceAction
	 */
	protected ActionMessage( Service service, Action serviceAction )
	{
		this.service = service;
		this.serviceAction = serviceAction;
		if( serviceAction.getInputActionArguments() != null )
		{
			inputParameters = new ArrayList<InputParamContainer>();
		}
	}

	/**
	 * Method to clear all set input parameters so that this object can
	 * be reused
	 */
	public void clearInputParameters()
	{
		inputParameters.clear();
	}

	/**
	 * Executes the message and retuns the UPNP device response,
	 * according to the UPNP specs, this method could take up to 30
	 * secs to process ( time allowed for a device to respond to a
	 * request )
	 * 
	 * @return a response object containing the UPNP parsed response
	 * @throws IOException
	 *            if some IOException occurs during message send and
	 *            reception process
	 * @throws UPNPResponseException
	 *            if an UPNP error message is returned from the server
	 *            or if some parsing exception occurs ( detailErrorCode
	 *            = 899, detailErrorDescription = SAXException message
	 *            )
	 */
	public ActionResponse service() throws IOException, UPNPResponseException
	{
		ActionResponse rtrVal = null;
		UPNPResponseException upnpEx = null;
		IOException ioEx = null;
		StringBuilder body = new StringBuilder( 256 );

		body.append( "<?xml version=\"1.0\"?>\r\n" );
		body.append( "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"" );
		body.append( " s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" );
		body.append( "<s:Body>" );
		body.append( "<u:" ).append( serviceAction.getName() ).append( " xmlns:u=\"" ).append(
				service.serviceType ).append( "\">" );

		if( serviceAction.getInputActionArguments() != null )
		{
			// this action requires params so we just set them...
			for( Iterator<InputParamContainer> itr = inputParameters.iterator(); itr.hasNext(); )
			{
				InputParamContainer container = itr.next();
				body.append( "<" ).append( container.name ).append( ">" ).append( container.value );
				body.append( "</" ).append( container.name ).append( ">" );
			}
		}
		body.append( "</u:" ).append( serviceAction.getName() ).append( ">" );
		body.append( "</s:Body>" );
		body.append( "</s:Envelope>" );

		URL url = new URL( service.controlURL.toString() );
		HttpURLConnection conn = ( HttpURLConnection ) url.openConnection();
		conn.setDoInput( true );
		conn.setDoOutput( true );
		conn.setUseCaches( false );
		conn.setRequestMethod( "POST" );
		conn.setInstanceFollowRedirects( false );
		// conn.setConnectTimeout( 30000 );
		conn.setRequestProperty( "HOST", url.getHost() + ":" + url.getPort() );
		conn.setRequestProperty( "CONTENT-TYPE", "text/xml; charset=\"utf-8\"" );
		conn.setRequestProperty( "CONTENT-LENGTH", Integer.toString( body.length() ) );
		conn.setRequestProperty( "SOAPACTION", "\"" + service.serviceType + "#"
				+ serviceAction.getName() + "\"" );
		OutputStream out = conn.getOutputStream();
		out.write( body.toString().getBytes() );
		out.flush();
		out.close();
		conn.connect();
		InputStream input = null;

		try
		{
			input = conn.getInputStream();
		}
		catch( IOException ex )
		{
			// java can throw an exception if he error code is 500 or 404
			// or something else than 200
			// but the device sends 500 error message with content that
			// is required
			// this content is accessible with the getErrorStream
			input = conn.getErrorStream();
		}

		if( input != null )
		{
			int response = conn.getResponseCode();
			String responseBody = getResponseBody( input );

			SAXParserFactory saxParFact = SAXParserFactory.newInstance();
			saxParFact.setValidating( false );
			saxParFact.setNamespaceAware( true );
			ActionMessageResponseParser msgParser = new ActionMessageResponseParser( serviceAction );
			StringReader stringReader = new StringReader( responseBody );
			InputSource src = new InputSource( stringReader );
			try
			{
				SAXParser parser = saxParFact.newSAXParser();
				parser.parse( src, msgParser );
			}
			catch( ParserConfigurationException confEx )
			{
				// should never happen
				// we throw a runtimeException to notify the env problem
				throw new RuntimeException(
						"ParserConfigurationException during SAX parser creation, please check your env settings:"
								+ confEx.getMessage() );
			}
			catch( SAXException saxEx )
			{
				// kind of tricky but better than nothing..
				upnpEx = new UPNPResponseException( 899, saxEx.getMessage() );
			}
			finally
			{
				try
				{
					input.close();
				}
				catch( IOException ex )
				{
					// ignore
				}
			}
			if( upnpEx == null )
			{
				if( response == HttpURLConnection.HTTP_OK )
				{
					rtrVal = msgParser.getActionResponse();
				}
				else if( response == HttpURLConnection.HTTP_INTERNAL_ERROR )
				{
					upnpEx = msgParser.getUPNPResponseException();
				}
				else
				{
					ioEx = new IOException( "Unexpected server HTTP response:" + response );
				}
			}
		}
		try
		{
			out.close();
		}
		catch( IOException ex )
		{
			// ignore
		}
		conn.disconnect();
		if( upnpEx != null )
		{
			throw upnpEx;
		}
		if( rtrVal == null && ioEx == null )
		{
			ioEx = new IOException( "Unable to receive a response from the UPNP device" );
		}
		if( ioEx != null )
		{
			throw ioEx;
		}
		return rtrVal;
	}

	private String getResponseBody( InputStream in ) throws IOException
	{
		byte[] buffer = new byte[ 256 ];
		int readen = 0;
		StringBuilder content = new StringBuilder( 256 );
		while( ( readen = in.read( buffer ) ) != -1 )
		{
			content.append( new String( buffer, 0, readen ) );
		}
		// some devices add \0 chars at XML message end
		// which causes XML parsing errors...
		int len = content.length();
		while( content.charAt( len - 1 ) == '\0' )
		{
			len--;
			content.setLength( len );
		}
		return content.toString().trim();
	}

	/**
	 * Set the value of an input parameter before a message service
	 * call. If the param name already exists, the param value will be
	 * overwritten with the new value provided.
	 * 
	 * @param parameterName
	 *           the parameter name
	 * @param parameterValue
	 *           the parameter value as an object, primitive object are
	 *           handled, all other object will be assigned with a call
	 *           to their toString() method call
	 * @return the current ActionMessage object instance
	 * @throws IllegalArgumentException
	 *            if the provided parameterName is not valid for this
	 *            message or if no input parameters are required for
	 *            this message
	 */
	public ActionMessage setInputParameter( String parameterName, Object parameterValue )
			throws IllegalArgumentException
	{
		if( parameterValue == null )
		{
			return setInputParameter( parameterName, "" );
		}
		else if( parameterValue instanceof Date )
		{
			return setInputParameter( parameterName, ( Date ) parameterValue );
		}
		else if( parameterValue instanceof Boolean )
		{
			return setInputParameter( parameterName, ( ( Boolean ) parameterValue ).booleanValue() );
		}
		else if( parameterValue instanceof Integer )
		{
			return setInputParameter( parameterName, ( ( Integer ) parameterValue ).intValue() );
		}
		else if( parameterValue instanceof Byte )
		{
			return setInputParameter( parameterName, ( ( Byte ) parameterValue ).byteValue() );
		}
		else if( parameterValue instanceof Short )
		{
			return setInputParameter( parameterName, ( ( Short ) parameterValue ).shortValue() );
		}
		else if( parameterValue instanceof Float )
		{
			return setInputParameter( parameterName, ( ( Float ) parameterValue ).floatValue() );
		}
		else if( parameterValue instanceof Double )
		{
			return setInputParameter( parameterName, ( ( Double ) parameterValue ).doubleValue() );
		}
		else if( parameterValue instanceof Long )
		{
			return setInputParameter( parameterName, ( ( Long ) parameterValue ).longValue() );
		}
		return setInputParameter( parameterName, parameterValue.toString() );
	}

	/**
	 * Set the value of an input parameter before a message service
	 * call. If the param name already exists, the param value will be
	 * overwritten with the new value provided
	 * 
	 * @param parameterName
	 *           the parameter name
	 * @param parameterValue
	 *           the string parameter value
	 * @return the current ActionMessage object instance
	 * @throws IllegalArgumentException
	 *            if the provided parameterName is not valid for this
	 *            message or if no input parameters are required for
	 *            this message
	 */
	public ActionMessage setInputParameter( String parameterName, String parameterValue )
			throws IllegalArgumentException
	{
		if( serviceAction.getInputActionArguments() == null )
		{
			throw new IllegalArgumentException( "No input parameters required for this message" );
		}
		Argument arg = serviceAction.getInputActionArgument( parameterName );
		if( arg == null )
		{
			throw new IllegalArgumentException( "Wrong input argument name for this action:"
					+ parameterName + " available parameters are : "
					+ serviceAction.getInputActionArguments() );
		}
		for( Iterator<InputParamContainer> i = inputParameters.iterator(); i.hasNext(); )
		{
			InputParamContainer container = i.next();
			if( container.name.equals( parameterName ) )
			{
				container.value = parameterValue;
				return this;
			}
		}
		// nothing found add the new value
		InputParamContainer container = new InputParamContainer();
		container.name = parameterName;
		container.value = parameterValue;
		inputParameters.add( container );
		return this;
	}

	/**
	 * Set the value of an input parameter before a message service
	 * call
	 * 
	 * @param parameterName
	 *           the parameter name
	 * @param parameterValue
	 *           the date parameter value, will be automatically
	 *           translated to the correct ISO 8601 date format for the
	 *           given action input param related state variable
	 * @return the current ActionMessage object instance
	 * @throws IllegalArgumentException
	 *            if the provided parameterName is not valid for this
	 *            message or if no input parameters are required for
	 *            this message
	 */
	public ActionMessage setInputParameter( String parameterName, Date parameterValue )
			throws IllegalArgumentException
	{
		if( serviceAction.getInputActionArguments() == null )
		{
			throw new IllegalArgumentException( "No input parameters required for this message" );
		}
		Argument arg = serviceAction.getInputActionArgument( parameterName );
		if( arg == null )
		{
			throw new IllegalArgumentException( "Wrong input argument name for this action:"
					+ parameterName + " available parameters are : "
					+ serviceAction.getInputActionArguments() );
		}
		StateVariable linkedVar = arg.getRelatedStateVariable();
		if( linkedVar.dataType.equals( StateVariableTypes.TIME ) )
		{
			return setInputParameter( parameterName, ISO8601Date.getIsoTime( parameterValue ) );
		}
		else if( linkedVar.dataType.equals( StateVariableTypes.TIME_TZ ) )
		{
			return setInputParameter( parameterName, ISO8601Date.getIsoTimeZone( parameterValue ) );
		}
		else if( linkedVar.dataType.equals( StateVariableTypes.DATE ) )
		{
			return setInputParameter( parameterName, ISO8601Date.getIsoDate( parameterValue ) );
		}
		else if( linkedVar.dataType.equals( StateVariableTypes.DATETIME ) )
		{
			return setInputParameter( parameterName, ISO8601Date.getIsoDateTime( parameterValue ) );
		}
		else if( linkedVar.dataType.equals( StateVariableTypes.DATETIME_TZ ) )
		{
			return setInputParameter( parameterName, ISO8601Date.getIsoDateTimeZone( parameterValue ) );
		}
		else
		{
			throw new IllegalArgumentException( "Related input state variable " + linkedVar.name
					+ " is not of an date type" );
		}
	}

	/**
	 * Set the value of an input parameter before a message service
	 * call
	 * 
	 * @param parameterName
	 *           the parameter name
	 * @param parameterValue
	 *           the boolean parameter value
	 * @return the current ActionMessage object instance
	 * @throws IllegalArgumentException
	 *            if the provided parameterName is not valid for this
	 *            message or if no input parameters are required for
	 *            this message
	 */
	public ActionMessage setInputParameter( String parameterName, boolean parameterValue )
			throws IllegalArgumentException
	{
		return setInputParameter( parameterName, parameterValue ? "1" : "0" );
	}

	/**
	 * Set the value of an input parameter before a message service
	 * call
	 * 
	 * @param parameterName
	 *           the parameter name
	 * @param parameterValue
	 *           the byte parameter value
	 * @return the current ActionMessage object instance
	 * @throws IllegalArgumentException
	 *            if the provided parameterName is not valid for this
	 *            message or if no input parameters are required for
	 *            this message
	 */
	public ActionMessage setInputParameter( String parameterName, byte parameterValue )
			throws IllegalArgumentException
	{
		return setInputParameter( parameterName, Byte.toString( parameterValue ) );
	}

	/**
	 * Set the value of an input parameter before a message service
	 * call
	 * 
	 * @param parameterName
	 *           the parameter name
	 * @param parameterValue
	 *           the short parameter value
	 * @return the current ActionMessage object instance
	 * @throws IllegalArgumentException
	 *            if the provided parameterName is not valid for this
	 *            message or if no input parameters are required for
	 *            this message
	 */
	public ActionMessage setInputParameter( String parameterName, short parameterValue )
			throws IllegalArgumentException
	{
		return setInputParameter( parameterName, Short.toString( parameterValue ) );
	}

	/**
	 * Set the value of an input parameter before a message service
	 * call
	 * 
	 * @param parameterName
	 *           the parameter name
	 * @param parameterValue
	 *           the integer parameter value
	 * @return the current ActionMessage object instance
	 * @throws IllegalArgumentException
	 *            if the provided parameterName is not valid for this
	 *            message or if no input parameters are required for
	 *            this message
	 */
	public ActionMessage setInputParameter( String parameterName, int parameterValue )
			throws IllegalArgumentException
	{
		return setInputParameter( parameterName, Integer.toString( parameterValue ) );
	}

	/**
	 * Set the value of an input parameter before a message service
	 * call
	 * 
	 * @param parameterName
	 *           the parameter name
	 * @param parameterValue
	 *           the long parameter value
	 * @return the current ActionMessage object instance
	 * @throws IllegalArgumentException
	 *            if the provided parameterName is not valid for this
	 *            message or if no input parameters are required for
	 *            this message
	 */
	public ActionMessage setInputParameter( String parameterName, long parameterValue )
			throws IllegalArgumentException
	{
		return setInputParameter( parameterName, Long.toString( parameterValue ) );
	}

	/**
	 * Set the value of an input parameter before a message service
	 * call
	 * 
	 * @param parameterName
	 *           the parameter name
	 * @param parameterValue
	 *           the float parameter value
	 * @return the current ActionMessage object instance
	 * @throws IllegalArgumentException
	 *            if the provided parameterName is not valid for this
	 *            message or if no input parameters are required for
	 *            this message
	 */
	public ActionMessage setInputParameter( String parameterName, float parameterValue )
			throws IllegalArgumentException
	{
		return setInputParameter( parameterName, Float.toString( parameterValue ) );
	}

	/**
	 * Set the value of an input parameter before a message service
	 * call
	 * 
	 * @param parameterName
	 *           the parameter name
	 * @param parameterValue
	 *           the double parameter value
	 * @return the current ActionMessage object instance
	 * @throws IllegalArgumentException
	 *            if the provided parameterName is not valid for this
	 *            message or if no input parameters are required for
	 *            this message
	 */
	public ActionMessage setInputParameter( String parameterName, double parameterValue )
			throws IllegalArgumentException
	{
		return setInputParameter( parameterName, Double.toString( parameterValue ) );
	}

	/**
	 * Input params class container
	 */
	private class InputParamContainer
	{

		private String name;

		private String value;

	}

}
