/*
  Copyright (C) 2006 Helge Hess

  This file is part of JOPE.

  JOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  JOPE is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with JOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

package org.opengroupware.jope.appserver.core;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.foundation.NSDisposable;
import org.opengroupware.jope.foundation.NSObject;
import org.opengroupware.jope.foundation.UString;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/*
 * WOMessage
 * 
 * Note:
 * Why do the write methods do not throw exceptions? Because 99% of the time
 * you write to a buffer and only a few times streaming is used (when delivering
 * large files / exports).
 * Using exceptions would result in a major complication of the rendering code.
 * 
 * Note:
 * We do not use constructors for WOMessage initialization. Use the appropriate
 * init() methods instead.
 */
public abstract class WOMessage extends NSObject
  implements CharSequence, Appendable, NSDisposable
{
  protected final static Log log = LogFactory.getLog("WOMessage");

  protected Map<String,List<String>> headers;
  protected Collection<WOCookie>     cookies;
  protected String httpVersion;
  protected byte[] contents;
  protected Map    userInfo;
  
  protected OutputStream outputStream;
  protected Exception    lastException;
  
  public WOMessage() {
  }
  public WOMessage(String _httpVersion, Map<String,List<String>> _headers, 
                   byte[] _contents, Map _userInfo)
  {
    this.init(_httpVersion, _headers, _contents, _userInfo);
  }
  
  public void init(String _httpVersion, Map<String,List<String>> _headers, 
                   byte[] _contents, Map _userInfo)
  {
    this.httpVersion = _httpVersion;
    this.headers     = _headers;
    this.contents    = _contents;
    this.userInfo    = _userInfo;
  }
  
  /* destructor */
  
  public void dispose() {
    this.httpVersion   = null;
    this.headers       = null;
    this.cookies       = null;
    this.contents      = null;
    this.userInfo      = null;
    this.lastException = null;
    
    if (this.outputStream != null) {
      try {
        this.outputStream.close();
      }
      catch (IOException e) {
        log.warn("failed to close output stream", e);
      }
      this.outputStream = null;
    }
  }
  
  /* accessors */
  
  public String httpVersion() {
    return this.httpVersion;
  }
  
  public void setUserInfo(Map _ui) {
    this.userInfo = _ui;
  }
  public Map userInfo() {
    return this.userInfo;
  }
  
  /* headers */
  
  public void setHeadersForKey(List<String> _v, String _key) {
    if (_v == null) {
      this.removeHeadersForKey(_key);
      return;
    }
    
    if (this.headers == null)
      this.headers = new HashMap<String, List<String>>(16);
    
    this.headers.put(_key, new ArrayList<String>(_v));
  }
  
  public void appendHeader(String _v, String _key) {
    if (_v == null || _key == null)
      return;
    if (this.headers == null)
      this.setHeaderForKey(_v, _key);
    else {    
      List<String> values = this.headers.get(_key);
      if (values == null) {
        values = new ArrayList<String>(1);
        this.headers.put(_key, values);
      }
      values.add(_v);
    }
  }
  
  public void removeHeadersForKey(String _key) {
    if (_key == null) return;
    if (this.headers == null) return;
    this.headers.remove(_key);
  }
  
  public List<String> headersForKey(String _key) {
    if (_key == null || this.headers == null) /* we never return null */
      return new ArrayList<String>(0);
    List<String> v = this.headers.get(_key);
    if (v == null) /* we never return null */
      return new ArrayList<String>(0);
    return v;
  }
  
  public Set<String> headerKeys() {
    if (this.headers == null) /* we never return null */
      return new HashSet<String>(0);
    return this.headers.keySet();
  }
  
  public void setHeaderForKey(String _v, String _key) {
    List<String> lheaders;
    if (_v == null)
      lheaders = null;
    else {
      lheaders = new ArrayList<String>(1);
      lheaders.add(_v);
    }
    this.setHeadersForKey(lheaders, _key);
  }
  public String headerForKey(String _key) {
    List<String> lheaders = this.headersForKey(_key);
    if (lheaders == null)
      return null;
    if (lheaders.size() == 0)
      return null;
    return lheaders.get(0);
  }
  
  public Map<String,List<String>> headers() {
    if (this.headers == null) /* we never return null */
      return new HashMap<String,List<String>>(0);
    return this.headers;
  }
  
  /* cookies */
  
  public Collection<WOCookie> cookies() {
    if (this.cookies == null) /* we never return null */
      return new ArrayList<WOCookie>(0);
    return this.cookies;
  }
  public void addCookie(WOCookie _cookie) {
    if (_cookie == null) return;
    if (this.cookies == null)
      this.cookies = new ArrayList<WOCookie>(4);
    this.cookies.add(_cookie);
  }
  public void removeCookie(WOCookie _cookie) {
    if (this.cookies == null) return;
    this.cookies.remove(_cookie);
  }
  
  /* fail status */
  
  public boolean didFail() {
    return this.lastException != null ? true : false;
  }
  public Exception lastException() {
    return this.lastException;
  }
  public void resetLastException() {
    this.lastException = null;
  }
  
  /* default encodings */
  
  protected static String defaultEncoding    = "latin1";
  protected static String defaultURLEncoding = "latin1";
  
  public static void setDefaultEncoding(String _v) {
    defaultEncoding = _v;
  }
  public static String defaultEncoding() {
    return defaultEncoding;
  }

  public static void setDefaultURLEncoding(String _v) {
    defaultURLEncoding = _v;
  }
  public static String defaultURLEncoding() {
    return defaultURLEncoding;
  }
  
  /* content representations */
  
  public String contentEncoding() {
    // TODO: keep in sync with OutputWriter!!
    return WOMessage.defaultEncoding();
  }
  
  public String contentString() {
    byte[] lcontent;
    
    if ((lcontent = this.content()) == null)
      return null;

    try {
      return new String(lcontent, 0, lcontent.length, this.contentEncoding());
    }
    catch (UnsupportedEncodingException uee) {
      this.lastException = uee;
      return null;
    }
  }
  
  /* content DOM support */

  static DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
  protected Document domDocument = null;
  
  public Document contentAsDOMDocument() {
    if (this.domDocument != null)
      return this.domDocument;

    DocumentBuilder db;
    
    if ((db = this.createDocumentBuilder()) == null)
      return null;
    
    try {
      // TODO: add support for streaming?
      this.domDocument = db.parse(this.contentString());
    }
    catch (SAXException e) {
      log.info("could not parse WOMessage content as DOM", e);
      return null;
    }
    catch (IOException e) {
      log.info("could not parse WOMessage content as DOM, IO error", e);
      return null;
    }
    
    return this.domDocument;
  }
  
  protected DocumentBuilder createDocumentBuilder() {
    try {
       return dbf.newDocumentBuilder();
    }
    catch (ParserConfigurationException e) {
      log.info("could not create DOM builder", e);
      return null;
    }
  }

  /* raw content handling */
  
  public boolean isStreaming() {
    return !(this.outputStream instanceof ByteArrayOutputStream);
  }
  
  public boolean enableStreaming() {
    /* this must be overridden in subclasses which allow streaming */
    return this.isStreaming();
  }
  
  public byte[] content() {
    if (this.contents != null)
      return this.contents;
    
    this.flush();
    
    if (!(this.outputStream instanceof ByteArrayOutputStream))
      return null; /* was a real stream */
    
    return ((ByteArrayOutputStream)this.outputStream).toByteArray();
  }
  
  public Exception flush() {
    if (this.outputStream == null)
      return null /* no error */;
    
    try {
      this.outputStream.flush();
      return null /* no error */;
    }
    catch (IOException ioe) {
      this.lastException = ioe;
      return ioe;
    }
  }
  
  public Exception appendContentData(byte[] _data, int _len) {
    if (_data == null || _len == 0)
      return null;
    
    try {
      this.outputStream.write(_data, 0 /* start-idx */, _len);
      return null; /* means: no error */
    }
    catch (IOException ioe) {
      return (this.lastException = ioe);
    }
  }
  
  public Exception appendContentString(String s) {
    if (s == null || s.length() == 0) return null;
    try {
      // TODO: optimize
      this.outputStream.write(s.getBytes(this.contentEncoding()));
      return null; /* means: no error */
    }
    catch (IOException ioe) {
      return (this.lastException = ioe);
    }
  }
  
  public Exception appendContentCharacter(char _c) {
    // TODO: optimize (probably add a [fast]StringBuffer)
    return this.appendContentString(new String(new char[] { _c }));
  }
  
  public Exception appendContentHTMLString(String s) {
    if (s == null) return null;
    return this.appendContentString(UString.stringByEscapingHTMLString(s));
  }

  public Exception appendContentHTMLAttributeValue(String s) {
    if (s == null) return null;
    s = UString.stringByEscapingHTMLAttributeValue(s);
    return this.appendContentString(s);
  }
  
  /* tag based writing */
  
  public Exception appendBeginTag(String _tagName) {
    return this.appendContentString("<" + _tagName);
  }
  public Exception appendBeginTagEnd() {
    return this.appendContentCharacter('>');
  }
  public Exception appendBeginTagClose() {
    return this.appendContentString(" />");
  }
  
  public Exception appendEndTag(String _tagName) {
    return this.appendContentString("</" + _tagName + ">");
  }
  
  public Exception appendAttribute(String _k, String _v) {
    Exception error;
    
    if ((error = this.appendContentCharacter(' ')) != null)
      return error;

    if ((error = this.appendContentString(_k)) != null)
      return error;

    if (_v != null) {
      if ((error = this.appendContentString("=\"")) != null)
        return error;

      if ((error = this.appendContentHTMLAttributeValue(_v)) != null)
        return error;

      if ((error = this.appendContentCharacter('"')) != null)
        return error;
    }
    // TODO: should we add a value if its missing? eg selected="selected"

    return null /* everything is alright */;
  }
  public Exception appendAttribute(String _k, int _v) {
    return this.appendAttribute(_k, "" + _v);
  }
  
  /* Escaping */
  
  public static String stringByEscapingHTMLString(String _v) {
    return UString.stringByEscapingHTMLString(_v);
  }
  public static String stringByEscapingHTMLAttributeValue(String _v) {
    return UString.stringByEscapingHTMLAttributeValue(_v);
  }
  
  /* Appendable */
  
  public Appendable append(CharSequence _s)
    throws IOException
  {
    this.appendContentHTMLString(_s.toString());
    return this;
  }
  public Appendable append(CharSequence _s, int _start, int _end)
    throws IOException
  {
    this.appendContentHTMLString(_s.subSequence(_start, _end).toString());
    return this;
  }
  public Appendable append(char _c)
    throws IOException
  {
    this.appendContentHTMLString(new String(new char[] { _c }));
    return this;
  }
  
  /* CharSequence */
  
  public char charAt(int _idx) {
    String s = this.contentString();
    return s != null ? s.charAt(_idx) : 0;
  }
  
  public int length() {
    String s = this.contentString();
    return s != null ? s.length() : 0;
  }
  
  public CharSequence subSequence(int _start, int _end) {
    String s = this.contentString();
    return s != null ? s.subSequence(_start, _end) : null;
  }
  
  public String toString() {
    return this.contentString();
  }
  
  /* HTTP status constants */
  
  public static final int HTTP_STATUS_OK                  = 200;
  public static final int HTTP_STATUS_CREATED             = 201;
  public static final int HTTP_STATUS_ACCEPTED            = 202;
  public static final int HTTP_STATUS_NO_CONTENT          = 204;

  public static final int HTTP_STATUS_MULTIPLE_CHOICES    = 300;
  public static final int HTTP_STATUS_MOVED_PERMANENTLY   = 301;
  public static final int HTTP_STATUS_FOUND               = 302;
  public static final int HTTP_STATUS_SEE_OTHER           = 303;
  public static final int HTTP_STATUS_NOT_MODIFIED        = 304;
  
  public static final int HTTP_STATUS_BAD_REQUEST         = 400;
  public static final int HTTP_STATUS_UNAUTHORIZED        = 401;
  public static final int HTTP_STATUS_PAYMENT_REQUIRED    = 402;
  public static final int HTTP_STATUS_FORBIDDEN           = 403;
  public static final int HTTP_STATUS_NOT_FOUND           = 404;
  public static final int HTTP_STATUS_METHOD_NOT_ALLOWED  = 405;
  public static final int HTTP_STATUS_NOT_ACCEPTABLE      = 406;
  
  public static final int HTTP_STATUS_INTERNAL_ERROR      = 500;
  public static final int HTTP_STATUS_NOT_IMPLEMENTED     = 501;
  public static final int HTTP_STATUS_SERVICE_UNAVAILABLE = 503;

  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    _d.append(" headers=" + this.headers);
  }
}
