/*
 * Copyright (c) 2001-2004 Caucho Technology, Inc.  All rights reserved.
 *
 * The Apache Software License, Version 1.1
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, 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 acknowlegement:
 *       "This product includes software developed by the
 *        Caucho Technology (http://www.caucho.com/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "Burlap", "Resin", and "Caucho" must not be used to
 *    endorse or promote products derived from this software without prior
 *    written permission. For written permission, please contact
 *    info@caucho.com.
 *
 * 5. Products derived from this software may not be called "Resin"
 *    nor may "Resin" appear in their names without prior written
 *    permission of Caucho Technology.
 *
 * 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 CAUCHO TECHNOLOGY OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, 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.
 *
 * @author Scott Ferguson
 */

package com.caucho.burlap.client;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;
import java.util.Date;
import java.util.Hashtable;
import java.util.TimeZone;
import java.util.Vector;

/**
 * Input stream for Burlap requests, compatible with microedition
 * Java.  It only uses classes and types available to J2ME.  In
 * particular, it does not have any support for the &lt;double> type.
 *
 * <p>MicroBurlapInput does not depend on any classes other than
 * in J2ME, so it can be extracted independently into a smaller package.
 *
 * <p>MicroBurlapInput is unbuffered, so any client needs to provide
 * its own buffering.
 *
 * <pre>
 * InputStream is = ...; // from http connection
 * MicroBurlapInput in = new MicroBurlapInput(is);
 * String value;
 *
 * in.startReply();         // read reply header
 * value = in.readString(); // read string value
 * in.completeReply();      // read reply footer
 * </pre>
 */
public class MicroBurlapInput {
  private static int base64Decode[];
  
  private InputStream is;
  protected int peek;
  protected boolean peekTag;
  protected Date date;
  protected Calendar utcCalendar;
  private Calendar localCalendar;
  protected Vector refs;
  protected String method;
  protected StringBuffer sbuf = new StringBuffer();
  protected StringBuffer entity = new StringBuffer();

  /**
   * Creates a new Burlap input stream, initialized with an
   * underlying input stream.
   *
   * @param is the underlying input stream.
   */
  public MicroBurlapInput(InputStream is)
  {
    init(is);
  }

  /**
   * Creates an uninitialized Burlap input stream.
   */
  public MicroBurlapInput()
  {
  }

  /**
   * Returns a call's method.
   */
  public String getMethod()
  {
    return method;
  }

  /**
   * Initialize the Burlap input stream with a new underlying stream.
   * Applications can use <code>init(InputStream)</code> to reuse
   * MicroBurlapInput to save garbage collection.
   */
  public void init(InputStream is)
  {
    this.is = is;
    this.refs = null;
  }

  /**
   * Starts reading the call
   *
   * <p>A successful completion will have a single value:
   *
   * <pre>
   * &lt;burlap:call>
   * &lt;method>method&lt;/method>
   * </pre>
   */
  public void startCall()
    throws IOException
  {
    expectStartTag("burlap:call");
    expectStartTag("method");
    method = parseString();
    expectEndTag("method");
    this.refs = null;
  }

  /**
   * Completes reading the call.
   *
   * <pre>
   * &lt;/burlap:call>
   * </pre>
   */
  public void completeCall()
    throws IOException
  {
    expectEndTag("burlap:call");
  }

  /**
   * Reads a reply as an object.
   * If the reply has a fault, throws the exception.
   */
  public Object readReply(Class expectedClass)
    throws Exception
  {
    if (startReply()) {
      Object value = readObject(expectedClass);
      completeReply();
      return value;
    }
    else {
      Hashtable fault = readFault();

      Object detail = fault.get("detail");
      if (detail instanceof Exception)
        throw (Exception) detail;

      else {
        String code = (String) fault.get("code");
        String message = (String) fault.get("message");
        
        throw new BurlapServiceException(message, code, detail);
      }
    }
  }

  /**
   * Starts reading the reply.
   *
   * <p>A successful completion will have a single value.  An unsuccessful
   * one will have a fault:
   *
   * <pre>
   * &lt;burlap:reply>
   * </pre>
   *
   * @return true if success, false for fault.
   */
  public boolean startReply()
    throws IOException
  {
    this.refs = null;
    
    expectStartTag("burlap:reply");

    if (! parseTag())
      throw new BurlapProtocolException("expected <value>");

    String tag = sbuf.toString();
    if (tag.equals("fault")) {
      peekTag = true;
      return false;
    }
    else {
      peekTag = true;
      return true;
    }
  }

  /**
   * Completes reading the reply.
   *
   * <pre>
   * &lt;/burlap:reply>
   * </pre>
   */
  public void completeReply()
    throws IOException
  {
    expectEndTag("burlap:reply");
  }

  /**
   * Reads a boolean value from the input stream.
   */
  public boolean readBoolean()
    throws IOException
  {
    expectStartTag("boolean");

    int value = parseInt();
    
    expectEndTag("boolean");

    return value != 0;
  }

  /**
   * Reads an integer value from the input stream.
   */
  public int readInt()
    throws IOException
  {
    expectStartTag("int");

    int value = parseInt();
    
    expectEndTag("int");

    return value;
  }

  /**
   * Reads a long value from the input stream.
   */
  public long readLong()
    throws IOException
  {
    expectStartTag("long");

    long value = parseLong();
    
    expectEndTag("long");

    return value;
  }

  /**
   * Reads a date value from the input stream.
   */
  public long readUTCDate()
    throws IOException
  {
    expectStartTag("date");

    if (utcCalendar == null)
      utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));

    long value = parseDate(utcCalendar);
    
    expectEndTag("date");

    return value;
  }

  /**
   * Reads a date value from the input stream.
   */
  public long readLocalDate()
    throws IOException
  {
    expectStartTag("date");

    if (localCalendar == null)
      localCalendar = Calendar.getInstance();

    long value = parseDate(localCalendar);
    
    expectEndTag("date");

    return value;
  }

  /**
   * Reads a remote value from the input stream.
   */
  public BurlapRemote readRemote()
    throws IOException
  {
    expectStartTag("remote");

    String type = readType();
    String url = readString();
    
    expectEndTag("remote");

    return new BurlapRemote(type, url);
  }

  /**
   * Reads a string value from the input stream.
   *
   * <p>The two valid possibilities are either a &lt;null>
   * or a &lt;string>.  The string value is encoded in utf-8, and
   * understands the basic XML escapes: "&123;", "&lt;", "&gt;",
   * "&apos;", "&quot;".
   *
   * <pre>
   * &lt;null>&lt;/null>
   * &lt;string>a utf-8 encoded string&lt;/string>
   * </pre>
   */
  public String readString()
    throws IOException
  {
    if (! parseTag())
      throw new BurlapProtocolException("expected <string>");

    String tag = sbuf.toString();
    if (tag.equals("null")) {
      expectEndTag("null");
      return null;
    }
    else if (tag.equals("string")) {
      sbuf.setLength(0);
      parseString(sbuf);
      String value = sbuf.toString();
      expectEndTag("string");
      return value;
    }
    else
      throw expectBeginTag("string", tag);
  }

  /**
   * Reads a byte array from the input stream.
   *
   * <p>The two valid possibilities are either a &lt;null>
   * or a &lt;base64>.
   */
  public byte []readBytes()
    throws IOException
  {
    if (! parseTag())
      throw new BurlapProtocolException("expected <base64>");

    String tag = sbuf.toString();
    if (tag.equals("null")) {
      expectEndTag("null");
      return null;
    }
    else if (tag.equals("base64")) {
      sbuf.setLength(0);
      byte []value = parseBytes();
      expectEndTag("base64");
      return value;
    }
    else
      throw expectBeginTag("base64", tag);
  }

  /**
   * Reads an arbitrary object the input stream.
   */
  public Object readObject(Class expectedClass)
    throws IOException
  {
    if (! parseTag())
      throw new BurlapProtocolException("expected <tag>");

    String tag = sbuf.toString();
    if (tag.equals("null")) {
      expectEndTag("null");
      return null;
    }
    else if (tag.equals("boolean")) {
      int value = parseInt();
      expectEndTag("boolean");
      return new Boolean(value != 0);
    }
    else if (tag.equals("int")) {
      int value = parseInt();
      expectEndTag("int");
      return new Integer(value);
    }
    else if (tag.equals("long")) {
      long value = parseLong();
      expectEndTag("long");
      return new Long(value);
    }
    else if (tag.equals("string")) {
      sbuf.setLength(0);
      parseString(sbuf);
      String value = sbuf.toString();
      expectEndTag("string");
      return value;
    }
    else if (tag.equals("xml")) {
      sbuf.setLength(0);
      parseString(sbuf);
      String value = sbuf.toString();
      expectEndTag("xml");
      return value;
    }
    else if (tag.equals("date")) {
      if (utcCalendar == null)
        utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
      
      long value = parseDate(utcCalendar);
      expectEndTag("date");
      return new Date(value);
    }
    else if (tag.equals("map")) {
      String type = readType();

      return readMap(expectedClass, type);
    }
    else if (tag.equals("list")) {
      String type = readType();
      
      int length = readLength();

      return readList(expectedClass, type, length);
    }
    else if (tag.equals("ref")) {
      int value = parseInt();
      expectEndTag("ref");

      return refs.elementAt(value);
    }
    else if (tag.equals("remote")) {
      String type = readType();
      String url = readString();

      expectEndTag("remote");

      return resolveRemote(type, url);
    }
    else
      return readExtensionObject(expectedClass, tag);
  }

  /**
   * Reads a type value from the input stream.
   *
   * <pre>
   * &lt;type>a utf-8 encoded string&lt;/type>
   * </pre>
   */
  public String readType()
    throws IOException
  {
    if (! parseTag())
      throw new BurlapProtocolException("expected <type>");

    String tag = sbuf.toString();
    if (! tag.equals("type"))
      throw new BurlapProtocolException("expected <type>");
    
    sbuf.setLength(0);
    parseString(sbuf);
    String value = sbuf.toString();
    expectEndTag("type");
    
    return value;
  }

  /**
   * Reads a length value from the input stream.  If the length isn't
   * specified, returns -1.
   *
   * <pre>
   * &lt;length>integer&lt;/length>
   * </pre>
   */
  public int readLength()
    throws IOException
  {
    expectStartTag("length");

    int ch = skipWhitespace();

    peek = ch;
    
    if (ch == '<') {
      expectEndTag("length");
      return -1;
    }

    int value = parseInt();
      
    expectEndTag("length");
    
    return value;
  }

  /**
   * Resolves a remote object.
   */
  public Object resolveRemote(String type, String url)
    throws IOException
  {
    return new BurlapRemote(type, url);
  }

  /**
   * Reads a fault.
   */
  public Hashtable readFault()
    throws IOException
  {
    expectStartTag("fault");
    
    Hashtable map = new Hashtable();

    while (parseTag()) {
      peekTag = true;
      Object key = readObject(null);
      Object value = readObject(null);

      if (key != null && value != null)
        map.put(key, value);
    }
    
    if (! sbuf.toString().equals("fault"))
      throw new BurlapProtocolException("expected </fault>");

    return map;
  }
  
  /**
   * Reads an object from the input stream.
   *
   * @param expectedClass the calling routine's expected class
   * @param type the type from the stream
   */
  public Object readMap(Class expectedClass, String type)
    throws IOException
  {
    Hashtable map = new Hashtable();
    if (refs == null)
      refs = new Vector();
    refs.addElement(map);

    while (parseTag()) {
      peekTag = true;
      Object key = readObject(null);
      Object value = readObject(null);

      map.put(key, value);
    }
    if (! sbuf.toString().equals("map"))
      throw new BurlapProtocolException("expected </map>");

    return map;
  }

  /**
   * Reads object unknown to MicroBurlapInput.
   */
  protected Object readExtensionObject(Class expectedClass, String tag)
    throws IOException
  {
    throw new BurlapProtocolException("unknown object tag <" + tag + ">");
  }
  
  /**
   * Reads a list object from the input stream.
   *
   * @param expectedClass the calling routine's expected class
   * @param type the type from the stream
   * @param length the expected length, -1 for unspecified length
   */
  public Object readList(Class expectedClass, String type, int length)
    throws IOException
  {
    Vector list = new Vector();
    if (refs == null)
      refs = new Vector();
    refs.addElement(list);

    while (parseTag()) {
      peekTag = true;
      Object value = readObject(null);

      list.addElement(value);
    }
    
    if (! sbuf.toString().equals("list"))
      throw new BurlapProtocolException("expected </list>");

    return list;
  }

  /**
   * Parses an integer value from the stream.
   */
  protected int parseInt()
    throws IOException
  {
    int sign = 1;
    int value = 0;

    int ch = skipWhitespace();
    if (ch == '+')
      ch = read();
    else if (ch == '-') {
      sign = -1;
      ch = read();
    }

    for (; ch >= '0' && ch <= '9'; ch = read())
      value = 10 * value + ch - '0';
    
    peek = ch;

    return sign * value;
  }

  /**
   * Parses a long value from the stream.
   */
  protected long parseLong()
    throws IOException
  {
    long sign = 1;
    long value = 0;

    int ch = skipWhitespace();
    if (ch == '+')
      ch = read();
    else if (ch == '-') {
      sign = -1;
      ch = read();
    }

    for (; ch >= '0' && ch <= '9'; ch = read()) {
      value = 10 * value + ch - '0';
    }

    peek = ch;

    return sign * value;
  }

  /**
   * Parses a date value from the stream.
   */
  protected long parseDate(Calendar calendar)
    throws IOException
  {
    int ch = skipWhitespace();
    
    int year = 0;
    for (int i = 0; i < 4; i++) {
      if (ch >= '0' && ch <= '9')
        year = 10 * year + ch - '0';
      else
        throw expectedChar("year", ch);

      ch = read();
    }

    int month = 0;
    for (int i = 0; i < 2; i++) {
      if (ch >= '0' && ch <= '9')
        month = 10 * month + ch - '0';
      else
        throw expectedChar("month", ch);

      ch = read();
    }

    int day = 0;
    for (int i = 0; i < 2; i++) {
      if (ch >= '0' && ch <= '9')
        day = 10 * day + ch - '0';
      else
        throw expectedChar("day", ch);

      ch = read();
    }

    if (ch != 'T')
      throw expectedChar("`T'", ch);

    ch = read();

    int hour = 0;
    for (int i = 0; i < 2; i++) {
      if (ch >= '0' && ch <= '9')
        hour = 10 * hour + ch - '0';
      else
        throw expectedChar("hour", ch);

      ch = read();
    }

    int minute = 0;
    for (int i = 0; i < 2; i++) {
      if (ch >= '0' && ch <= '9')
        minute = 10 * minute + ch - '0';
      else
        throw expectedChar("minute", ch);

      ch = read();
    }

    int second = 0;
    for (int i = 0; i < 2; i++) {
      if (ch >= '0' && ch <= '9')
        second = 10 * second + ch - '0';
      else
        throw expectedChar("second", ch);

      ch = read();
    }

    for (; ch > 0 && ch != '<'; ch = read()) {
    }

    peek = ch;

    calendar.set(Calendar.YEAR, year);
    calendar.set(Calendar.MONTH, month - 1);
    calendar.set(Calendar.DAY_OF_MONTH, day);
    calendar.set(Calendar.HOUR_OF_DAY, hour);
    calendar.set(Calendar.MINUTE, minute);
    calendar.set(Calendar.SECOND, second);
    calendar.set(Calendar.MILLISECOND, 0);

    return calendar.getTime().getTime();
  }

  /**
   * Parses a string value from the stream.
   * string buffer is used for the result.
   */
  protected String parseString()
    throws IOException
  {
    StringBuffer sbuf = new StringBuffer();

    return parseString(sbuf).toString();
  }
  
  /**
   * Parses a string value from the stream.  The burlap object's
   * string buffer is used for the result.
   */
  protected StringBuffer parseString(StringBuffer sbuf)
    throws IOException
  {
    int ch = read();

    for (; ch >= 0 && ch != '<'; ch = read()) {
      if (ch == '&') {
        ch = read();

        if (ch == '#') {
          ch = read();

          if (ch >= '0' && ch <= '9') {
            int v = 0;
            for (; ch >= '0' && ch <= '9'; ch = read()) {
              v = 10 * v + ch - '0';
            }

            sbuf.append((char) v);
          }
        }
        else {
          StringBuffer entityBuffer = new StringBuffer();

          for (; ch >= 'a' && ch <= 'z'; ch = read())
            entityBuffer.append((char) ch);

          String entity = entityBuffer.toString();
          if (entity.equals("amp"))
            sbuf.append('&');
          else if (entity.equals("apos"))
            sbuf.append('\'');
          else if (entity.equals("quot"))
            sbuf.append('"');
          else if (entity.equals("lt"))
            sbuf.append('<');
          else if (entity.equals("gt"))
            sbuf.append('>');
          else
            throw new BurlapProtocolException("unknown XML entity &" + entity + "; at `" + (char) ch + "'");
        }

        if (ch != ';')
          throw expectedChar("';'", ch);
      }
      else if (ch < 0x80)
        sbuf.append((char) ch);
      else if ((ch & 0xe0) == 0xc0) {
        int ch1 = read();
        int v = ((ch & 0x1f) << 6) + (ch1 & 0x3f);

        sbuf.append((char) v);
      }
      else if ((ch & 0xf0) == 0xe0) {
        int ch1 = read();
        int ch2 = read();
        int v = ((ch & 0x0f) << 12) + ((ch1 & 0x3f) << 6) + (ch2 & 0x3f);

        sbuf.append((char) v);
      }
      else
        throw new BurlapProtocolException("bad utf-8 encoding");
    }

    peek = ch;

    return sbuf;
  }
  
  /**
   * Parses a byte array.
   */
  protected byte []parseBytes()
    throws IOException
  {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();

    parseBytes(bos);

    return bos.toByteArray();
  }
  
  /**
   * Parses a byte array.
   */
  protected ByteArrayOutputStream parseBytes(ByteArrayOutputStream bos)
    throws IOException
  {
    int ch;
    for (ch = read(); ch >= 0 && ch != '<'; ch = read()) {
      int b1 = ch;
      int b2 = read();
      int b3 = read();
      int b4 = read();

      if (b4 != '=') {
        int chunk = ((base64Decode[b1] << 18) +
                     (base64Decode[b2] << 12) +
                     (base64Decode[b3] << 6) +
                     (base64Decode[b4]));

        bos.write(chunk >> 16);
        bos.write(chunk >> 8);
        bos.write(chunk);
      }
      else if (b3 != '=') {
        int chunk = ((base64Decode[b1] << 12) +
                     (base64Decode[b2] << 6) +
                     (base64Decode[b3]));

        bos.write(chunk >> 8);
        bos.write(chunk);
      }
      else {
        int chunk = ((base64Decode[b1] << 6) +
                     (base64Decode[b2]));

        bos.write(chunk);
      }
    }

    if (ch == '<')
      peek = ch;
    
    return bos;
  }

  protected void expectStartTag(String tag)
    throws IOException
  {
    if (! parseTag())
      throw new BurlapProtocolException("expected <" + tag + ">");

    if (! sbuf.toString().equals(tag))
      throw new BurlapProtocolException("expected <" + tag + "> at <" + sbuf + ">");
  }

  protected void expectEndTag(String tag)
    throws IOException
  {
    if (parseTag())
      throw new BurlapProtocolException("expected </" + tag + ">");

    if (! sbuf.toString().equals(tag))
      throw new BurlapProtocolException("expected </" + tag + "> at </" + sbuf + ">");
  }

  /**
   * Parses a tag.  Returns true if it's a start tag.
   */
  protected boolean parseTag()
    throws IOException
  {
    if (peekTag) {
      peekTag = false;
      return true;
    }
    
    int ch = skipWhitespace();
    boolean isStartTag = true;

    if (ch != '<')
      throw expectedChar("'<'", ch);

    ch = read();
    if (ch == '/') {
      isStartTag = false;
      ch = is.read();
    }
    
    if (! isTagChar(ch))
      throw expectedChar("tag", ch);
      
    sbuf.setLength(0);
    for (; isTagChar(ch); ch = read())
      sbuf.append((char) ch);

    if (ch != '>')
      throw expectedChar("'>'", ch);

    return isStartTag;
  }

  protected IOException expectedChar(String expect, int actualChar)
  {
    return new BurlapProtocolException("expected " + expect + " at " +
                           (char) actualChar + "'");
  }

  protected IOException expectBeginTag(String expect, String tag)
  {
    return new BurlapProtocolException("expected <" + expect + "> at <" + tag + ">");
  }

  private boolean isTagChar(int ch)
  {
    return (ch >= 'a' && ch <= 'z' ||
            ch >= 'A' && ch <= 'Z' ||
            ch >= '0' && ch <= '9' ||
            ch == ':' || ch == '-');
  }

  protected int skipWhitespace()
    throws IOException
  {
    int ch = read();

    for (;
         ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r';
         ch = read()) {
    }

    return ch;
  }

  protected boolean isWhitespace(int ch)
    throws IOException
  {
    return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r';
  }

  protected int read()
    throws IOException
  {
    if (peek > 0) {
      int value = peek;
      peek = 0;
      return value;
    }
    
    return is.read();
  }

  static {
    base64Decode = new int[256];
    for (int i = 'A'; i <= 'Z'; i++)
      base64Decode[i] = i - 'A';
    for (int i = 'a'; i <= 'z'; i++)
      base64Decode[i] = i - 'a' + 26;
    for (int i = '0'; i <= '9'; i++)
      base64Decode[i] = i - '0' + 52;
    base64Decode['+'] = 62;
    base64Decode['/'] = 63;
  }
}
