/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2002 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.ui.rpc;

import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.log4j.*;
import org.w3c.dom.*;
import com.silverwrist.util.*;
import com.silverwrist.venice.core.*;
import com.silverwrist.venice.except.*;
import com.silverwrist.venice.ui.*;
import com.silverwrist.venice.util.XMLLoader;

public class XmlRpcRequest
{
  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static Category logger = Category.getInstance(XmlRpcRequest.class);

  private static SimpleTimeZone utc = new SimpleTimeZone(0,"UTC");

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  RequestInput req;            // request input system
  String method_name;          // the method name
  List method_params;          // the method parameters

  /*--------------------------------------------------------------------------------
   * Constructors
   *--------------------------------------------------------------------------------
   */

  XmlRpcRequest(RequestInput req, Document req_doc) throws XmlRpcFault
  {
    if (req_doc==null)
      throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,"no XML call structure found");

    try
    { // load the initial structure and method name
      XMLLoader loader = XMLLoader.get();
      Element root = loader.postGetRootElement(req_doc,"methodCall");
      DOMElementHelper root_h = new DOMElementHelper(root);
      method_name = loader.postGetSubElementText(root_h,"methodName");

      // parse the parameters
      Element params = root_h.getSubElement("params");
      ArrayList tmp_method_params = new ArrayList();
      if (params!=null)
      { // look for <param/> children of the <params/> node
	NodeList nl = params.getChildNodes();
	for (int i=0; i<nl.getLength(); i++)
	{ // look at the child node...
	  Node n = nl.item(i);
	  if (n.getNodeType()==Node.ELEMENT_NODE)
	  { // make sure it's a <param/> node
	    if (!(n.getNodeName().equals("param")))
	      throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,
				    "non-param element found inside \"params\" element");
	    tmp_method_params.add(parseValue(loader.postGetSubSection((Element)n,"value")));

	  } // end if
	  // else ignore this node

	} // end for

      } // end if

      // save the method parameters
      if (tmp_method_params.isEmpty())
	method_params = Collections.EMPTY_LIST;
      else
      { // make it read-only before 
	tmp_method_params.trimToSize();
	method_params = Collections.unmodifiableList(tmp_method_params);

      } // end else

    } // end try
    catch (ValidationException ve)
    { // validation exceptions get translated to XML-RPC faults
      throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,ve);

    } // end catch

    this.req = req;  // save reference off

  } // end constructor

  XmlRpcRequest(RequestInput req, String method_name, List method_params)
  {
    this.req = req;
    this.method_name = method_name;
    this.method_params = method_params;

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal static operations
   *--------------------------------------------------------------------------------
   */

  private static final Object parseValue(Element val) throws ValidationException, XmlRpcFault
  {
    XMLLoader loader = XMLLoader.get();

    // see if the value has an embedded type...
    NodeList nl = val.getChildNodes();
    Element type = null;
    for (int i=0; i<nl.getLength(); i++)
    { // look for any sub-element within a value
      Node n = nl.item(i);
      if (n.getNodeType()==Node.ELEMENT_NODE)
      { // save off the type-specific element
	if (type!=null)
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,
				"more than one type-specific element within a \"value\" element");
	type = (Element)n;

      } // end if

    } // end for

    if (type!=null)
    { // look at the contents of the "type" element
      DOMElementHelper h = new DOMElementHelper(type);
      final String name = type.getTagName();
      if (name.equals("i4") || name.equals("int"))
      { // parse an integer value
	String s = h.getElementText();
	if (s==null)
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,"invalid null inside \"" + name + "\" value");
	Integer rc = null;
	try
	{ // create the return value
	  rc = new Integer(s.trim());

	} // end try
	catch (NumberFormatException nfe)
	{ // bogus number!
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,"invalid integer value inside \"" + name
				                            + "\" value");

	} // end catch

	return rc;

      } // end if
      else if (name.equals("boolean"))
      { // parse a boolean value
	String s = h.getElementText();
	if (s==null)
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,"invalid null inside \"boolean\" value");
	s = s.trim();
	if (s.equals("1"))
	  return Boolean.TRUE;
	else if (s.equals("0"))
	  return Boolean.FALSE;
	else
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,"invalid value inside \"boolean\" value");

      } // end else if
      else if (name.equals("string"))
      { // parse a string value
	String s = h.getElementText();
	if (s==null)
	  s = "";
	return s;

      } // end else if
      else if (name.equals("double"))
      { // parse a double-precision floating-point value
	String s = h.getElementText();
	if (s==null)
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,"invalid null inside \"double\" value");
	Double rc = null;
	try
	{ // create the return value
	  rc = new Double(s.trim());

	} // end try
	catch (NumberFormatException nfe)
	{ // bogus number!
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,
				"invalid floating-point value inside \"double\" value");

	} // end catch

	return rc;

      } // end else if
      else if (name.equals("dateTime.iso8601"))
      { // parse a date/time value
	String dstr = h.getElementText();
	if (dstr==null)
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,"invalid null inside \"dateTime.iso8601\" value");
	dstr = dstr.trim();

	// run some validation checks on the string
	if ((dstr.charAt(8)!='T') || (dstr.charAt(11)!=':') || (dstr.charAt(14)!=':'))
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,
				"invalid ISO8601 date/time value inside \"dateTime.iso8601\" value");

	try
	{ // use a GregorianCalendar to convert the fields into the appropriate format
	  GregorianCalendar cal = new GregorianCalendar(utc);
	  cal.set(Calendar.YEAR,Integer.parseInt(dstr.substring(0,4)));
	  cal.set(Calendar.MONTH,Integer.parseInt(dstr.substring(4,6)) - 1 + Calendar.JANUARY);
	  cal.set(Calendar.DAY_OF_MONTH,Integer.parseInt(dstr.substring(6,8)));
	  cal.set(Calendar.HOUR_OF_DAY,Integer.parseInt(dstr.substring(9,11)));
	  cal.set(Calendar.MINUTE,Integer.parseInt(dstr.substring(12,14)));
	  cal.set(Calendar.SECOND,Integer.parseInt(dstr.substring(15,17)));
	  return cal.getTime();
	  
	} // end try
	catch (NumberFormatException nfe)
	{ // numeric conversion error!
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,
				"invalid ISO8601 date/time value inside \"dateTime.iso8601\" value");

	} // end catch

      } // end else if
      else if (name.equals("base64"))
      { // parse a binary value
	String s = h.getElementText();
	if (s==null)
	  return new byte[0];
	byte[] encoded_data = null;
	try
	{ // get the encoded data
	  encoded_data = s.getBytes("US-ASCII");

	} // end try
	catch (UnsupportedEncodingException uee)
	{ // this is bogus
	  throw new InternalStateError("US-ASCII reports as 'unsupported'...shouldn't happen");

	} // end catch

	byte[] rc = null;
	try
	{ // wrap the byte array in a stream and decode it
	  // note: we use JavaMail's MIME decoder here
	  ByteArrayInputStream encoded_stm = new ByteArrayInputStream(encoded_data);
	  InputStream decoded_stm = MimeUtility.decode(encoded_stm,"base64");

	  // copy the decoded information to a new stream
	  ByteArrayOutputStream out_stm = new ByteArrayOutputStream();
	  IOUtil.copy(decoded_stm,out_stm);
	  IOUtil.shutdown(decoded_stm);
	  IOUtil.shutdown(encoded_stm);

	  // save the byte array backing the new stream
	  rc = out_stm.toByteArray();
	  IOUtil.shutdown(out_stm);

	} // end try
	catch (IOException ioe)
	{ // error schlepping all those bits around
	  throw new XmlRpcFault(XmlRpcFault.INTERNAL_ERROR,
				"unable to load binary data from \"base64\" value");

	} // end catch
	catch (MessagingException me)
	{ // invalid format
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,"invalid encoded data inside \"base64\" value");

	} // end catch

	return rc;

      } // end else if
      else if (name.equals("struct"))
	return parseStruct(type);
      else if (name.equals("array"))
	return parseArray(type);
      else
      { // no dice here
	logger.error("inside a <value/> element: expected a valid type but got a <" + name + "/>");
	throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,
			      "invalid type element \"" + name + "\" specified inside a \"value\"");

      } // end else

    } // end if
    else
    { // if there's no type-specific element, treat it as a string
      DOMElementHelper h = new DOMElementHelper(val);
      String s = h.getElementText();
      if (s==null)
	s = "";
      return s;

    } // end else

  } // end parseValue

  private static final List parseArray(Element val) throws XmlRpcFault, ValidationException
  {
    XMLLoader loader = XMLLoader.get();
    Element data = loader.postGetSubSection(val,"data");
    NodeList nl = data.getChildNodes();
    ArrayList rc = new ArrayList();
    for (int i=0; i<nl.getLength(); i++)
    { // look for <value/> elements within the <data/> element
      Node n = nl.item(i);
      if (n.getNodeType()==Node.ELEMENT_NODE)
      { // make sure we've got a value element here!
	if (!(n.getNodeName().equals("value")))
	{ // this is a bogus value!
	  logger.error("inside array <data/>: expected a <value/> but found a <" + n.getNodeName() + "/>");
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,
				"non-value element found inside array \"data\" element");

	} // end if

	rc.add(parseValue((Element)n));

      } // end if

    } // end for

    if (rc.isEmpty())
      return Collections.EMPTY_LIST;
    else
    { // trim the result array and return the list
      rc.trimToSize();
      return Collections.unmodifiableList(rc);

    } // end else

  } // end parseArray

  private static final Map parseStruct(Element val) throws XmlRpcFault, ValidationException
  {
    XMLLoader loader = XMLLoader.get();
    NodeList nl = val.getChildNodes();
    HashMap rc = new HashMap();
    for (int i=0; i<nl.getLength(); i++)
    { // seek out all the <member/> elements within a <struct/> element
      Node n = nl.item(i);
      if (n.getNodeType()==Node.ELEMENT_NODE)
      { // make sure we've got a value element here!
	if (!(n.getNodeName().equals("member")))
	{ // this is a bogus value
	  logger.error("inside <struct/>: expected a <member/> but found a <" + n.getNodeName() + "/>");
	  throw new XmlRpcFault(XmlRpcFault.INVALID_REQUEST,
				"non-member element found inside \"struct\" element");

	} // end if

	DOMElementHelper h = new DOMElementHelper((Element)n);
	String my_name = loader.postGetSubElementText(h,"name").trim();
	Element my_val = loader.postGetSubSection(h,"value");
	rc.put(my_name,parseValue(my_val));

      } // end if

    } // end for

    if (rc.isEmpty())
      return Collections.EMPTY_MAP;
    else
      return Collections.unmodifiableMap(rc);

  } // end parseStruct

  /*--------------------------------------------------------------------------------
   * External operations
   *--------------------------------------------------------------------------------
   */

  public final String getMethod()
  {
    return method_name;

  } // end getMethod

  public final int getParamCount()
  {
    return method_params.size();

  } // end getParamCount

  public final List getParams()
  {
    return method_params;

  } // end getParams

  public final Object getParam(int ndx)
  {
    return method_params.get(ndx);

  } // end getParam

  public final String getParamType(int ndx)
  {
    Object foo = method_params.get(ndx);
    if (foo instanceof Integer)
      return "int";
    if (foo instanceof Boolean)
      return "boolean";
    if (foo instanceof String)
      return "string";
    if (foo instanceof Double)
      return "double";
    if (foo instanceof Date)
      return "dateTime";
    if (foo instanceof byte[])
      return "base64";
    if (foo instanceof Map)
      return "struct";
    if (foo instanceof List)
      return "array";
    return "(unknown)";

  } // end getParamType

  public final int getParamInt(int ndx) throws XmlRpcFault
  {
    Object foo = method_params.get(ndx);
    if (foo instanceof Integer)
      return ((Integer)foo).intValue();
    else if (foo instanceof Boolean)
      return ((Boolean)foo).booleanValue() ? 1 : 0;
    else
      throw new XmlRpcFault(XmlRpcFault.INVALID_PARAMS,"parameter type mismatch");

  } // end getParamInt

  public final double getParamDouble(int ndx) throws XmlRpcFault
  {
    Object foo = method_params.get(ndx);
    if ((foo instanceof Integer) || (foo instanceof Double))
      return ((Number)foo).doubleValue();
    else if (foo instanceof Boolean)
      return ((Boolean)foo).booleanValue() ? 1 : 0;
    else
      throw new XmlRpcFault(XmlRpcFault.INVALID_PARAMS,"parameter type mismatch");

  } // end getParamDouble

  public final String getParamString(int ndx) throws XmlRpcFault
  {
    Object foo = method_params.get(ndx);
    if ((foo instanceof byte[]) || (foo instanceof List) || (foo instanceof Map))
      throw new XmlRpcFault(XmlRpcFault.INVALID_PARAMS,"parameter type mismatch");

    return foo.toString();

  } // end getParamString

  public final CommunityContext getParamCommunity(int ndx) throws XmlRpcFault, DataException
  {
    Object foo = method_params.get(ndx);
    if ((foo instanceof byte[]) || (foo instanceof List) || (foo instanceof Map) || (foo instanceof Boolean))
      throw new XmlRpcFault(XmlRpcFault.INVALID_PARAMS,"parameter type mismatch");
    if (foo instanceof Integer)
      return req.getUser().getCommunityContext(((Integer)foo).intValue());
    else
      return req.getUser().getCommunityContext(foo.toString().trim());

  } // end getParamCommunity

  public final ConferenceContext getParamConference(int ndx, CommunityContext comm)
      throws XmlRpcFault, DataException, AccessError
  {
    Object foo = method_params.get(ndx);
    if ((foo instanceof byte[]) || (foo instanceof List) || (foo instanceof Map) || (foo instanceof Boolean))
      throw new XmlRpcFault(XmlRpcFault.INVALID_PARAMS,"parameter type mismatch");
    if (foo instanceof Integer)
      return comm.getConferenceContext(((Integer)foo).intValue());
    else
      return comm.getConferenceContext(foo.toString().trim());

  } // end getParamConference

  public final TopicContext getParamTopic(int ndx, ConferenceContext conf)
      throws XmlRpcFault, DataException, AccessError
  {
    Object foo = method_params.get(ndx);
    if (!(foo instanceof Integer))
      throw new XmlRpcFault(XmlRpcFault.INVALID_PARAMS,"parameter type mismatch");
    return conf.getTopic(((Integer)foo).shortValue());

  } // end getParamTopic

  public final TopicMessageContext getParamPost(int ndx, TopicContext topic)
      throws XmlRpcFault, DataException, AccessError
  {
    Object foo = method_params.get(ndx);
    if (!(foo instanceof Integer))
      throw new XmlRpcFault(XmlRpcFault.INVALID_PARAMS,"parameter type mismatch");
    return topic.getMessage(((Integer)foo).intValue());

  } // end getParamPost

} // end class XmlRpcRequest
