/*
  Copyright (C) 2007 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.publisher;

import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Iterator;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.appserver.core.WOActionResults;
import org.opengroupware.jope.appserver.core.WOApplication;
import org.opengroupware.jope.appserver.core.WOComponent;
import org.opengroupware.jope.appserver.core.WOContext;
import org.opengroupware.jope.appserver.core.WOElement;
import org.opengroupware.jope.appserver.core.WOMessage;
import org.opengroupware.jope.appserver.core.WORequest;
import org.opengroupware.jope.appserver.core.WOResponse;
import org.opengroupware.jope.foundation.NSException;
import org.opengroupware.jope.foundation.NSJavaRuntime;
import org.opengroupware.jope.foundation.NSObject;
import org.opengroupware.jope.foundation.UString;

/**
 * JoDefaultRenderer
 * <p>
 * This is the default renderer which can deal with all the regular call
 * results. That is WOResponse, WOComponent, String, etc.
 * <p>
 * @see IJoObjectRenderer
 */
public class JoDefaultRenderer extends NSObject implements IJoObjectRenderer {
  protected static final Log log = LogFactory.getLog("JoDefaultRenderer");
  
  public static final JoDefaultRenderer sharedRenderer =
    new JoDefaultRenderer();
  
  protected IJoObjectRenderer jsonRenderer;
  
  public JoDefaultRenderer() {
    this.jsonRenderer = new JoSimpleJSONRenderer();
  }

  /* control rendering */
  
  /**
   * Checks whether the JoDefaultRenderer can render the given object. This
   * renderer can render JSON stuff plus:
   * <ul>
   *   <li>WOComponent
   *   <li>WOResponse
   *   <li>WOActionResults
   *   <li>WOElement
   *   <li>String
   *   <li>Exception
   *   <li>WOApplication (render a HTTP redirect to the entry URL)
   * </ul>
   */
  public boolean canRenderObjectInContext(Object _object, WOContext _ctx) {
    if (this.jsonRenderer.canRenderObjectInContext(_object, _ctx))
      return true;
    
    if (_object instanceof WOComponent     ||
        _object instanceof WOResponse      ||
        _object instanceof WOActionResults ||
        _object instanceof WOElement       ||
        _object instanceof String          ||
        _object instanceof Exception       ||
        _object instanceof WOApplication   ||
        _object instanceof BufferedImage   ||
        _object instanceof Image)
      return true;

    return false;
  }
  
  /* rendering */

  public Exception renderObjectInContext(Object _object, WOContext _ctx) {
    if (this.jsonRenderer.canRenderObjectInContext(_object, _ctx))
      return this.jsonRenderer.renderObjectInContext(_object, _ctx);
      
    if (_object instanceof WOComponent)
      return this.renderComponent((WOComponent)_object, _ctx);
    
    if (_object instanceof WOResponse)
      return this.renderResponse((WOResponse)_object, _ctx);
    
    if (_object instanceof WOActionResults)
      return this.renderActionResults((WOActionResults)_object, _ctx);
    
    if (_object instanceof Exception)
      return this.renderException((Exception)_object, _ctx);
    
    if (_object instanceof WOElement)
      return this.renderElement((WOElement)_object, _ctx);
    
    if (_object instanceof String)
      return this.renderString(_object, _ctx);
    
    if (_object instanceof BufferedImage)
      return this.renderBufferedImage((BufferedImage)_object, _ctx);
    
    if (_object instanceof Image)
      return this.renderImage((Image)_object, _ctx);
    
    /* standard fallback for WOApplication */
    
    if (_object instanceof WOApplication) {
      /* This is if someone enters the root URL, per default we either redirect
       * to the DirectAction or to the Main page.
       */
      WOResponse r = ((WOApplication)_object).redirectToApplicationEntry(_ctx);
      return this.renderResponse(r, _ctx);
    }
    
    /* failed rendering, should not happen with proper call to canRender.. */
    
    log.error("cannot render object: " + _object);
    return new JoInternalErrorException("cannot render given object");
  }
  
  /* specific renderers */
  
  public Exception renderResponse(WOResponse _response, WOContext _ctx) {
    if (_response == null)
      return new JoInternalErrorException("got no response to render");
    
    if (_ctx.response() == _response) /* response is already active */
      return null; /* everything OK */
    
    // TODO
    log.error("custom WOResponse'es not yet supported: " + _response);
    return new NSException("unimplemented: cannot render response");
  }

  public Exception renderException(Exception _exception, WOContext _ctx) {
    if (_exception == null)
      return new JoInternalErrorException("got no exception to render");
    
    /* determine status */
    
    int httpStatus = NSJavaRuntime.intValueForKey(_exception, "httpStatus");
    if (httpStatus < 100 || httpStatus > 999) {
      if (httpStatus != 0) {
        log.warn("got invalid httpStatus " + httpStatus + " for exception: " +
                 _exception);
      }
      httpStatus = WOMessage.HTTP_STATUS_INTERNAL_ERROR;
    }
    if (httpStatus == 500)
      log.warn("delivering internal error exception", _exception);
    
    /* render exception */
    
    WOResponse r = _ctx.response();
    r.setStatus(httpStatus);
    
    // TODO: we should check the request 'accept' header and then decide
    r.setHeaderForKey("text/html", "content-type");
    
    // TODO: we might want to render more
    
    JoTraversalPath tp = _ctx.joTraversalPath();
    if (tp != null) {
      r.appendContentString("Path: ");
      r.appendContentHTMLString
        (UString.componentsJoinedByString(tp.path(), " => "));
      r.appendContentString("<br />");
    }
    
    r.appendContentHTMLString
      ("Exception: " + _exception.getClass().getCanonicalName());
    r.appendContentString("<br />");
    
    r.appendContentHTMLString("Cause: " + _exception.getCause());
    r.appendContentString("<br />");
    
    r.appendContentHTMLString("Message: " + _exception.getMessage());
    r.appendContentString("<br />");
    
    return null /* everything is great */;
  }

  public Exception renderComponent(WOComponent _page, WOContext _ctx) {
    /* reuse context response for WOComponent */
    WOResponse r = _ctx.response();
    if (_page == null)
      return new JoInternalErrorException("got no page to render");
    
    if (log.isDebugEnabled()) log.debug("delivering page: " + _page);
    
    _ctx.setPage(_page);
    _page.ensureAwakeInContext(_ctx);
    _ctx.enterComponent(_page, null /* component-content */);
    // TBD: shouldn't we call WOApplication appendToResponse?!
    _page.appendToResponse(r, _ctx);
    _ctx.leaveComponent(_page);
    
    return null /* everything OK */;
  }
  
  public Exception renderElement(WOElement _e, WOContext _ctx) {
    if (_e == null)
      return new JoInternalErrorException("got no element to render");
    
    WOResponse r = _ctx.response();
    _e.appendToResponse(r, _ctx);
    return null /* everything OK */;
  }
  
  public Exception renderActionResults(WOActionResults _r, WOContext _ctx) {
    if (_r == null)
      return new JoInternalErrorException("got no actionresults to render");
    
    WOResponse r = _r.generateResponse();
    if (r == null) {
      return new JoInternalErrorException
      ("got no actionresults response to render");
    }

    return this.renderResponse(r, _ctx);
  }
  
  public Exception renderString(Object _o, WOContext _ctx) {
    String s = _o.toString();
    if (s == null)
      return new JoInternalErrorException("got no string to render");

    WOResponse r = _ctx.response();
    r.setStatus(WOMessage.HTTP_STATUS_OK);
    // TODO: we should check the request 'accept' header and then decide
    r.setHeaderForKey("text/html", "content-type");
    r.appendContentHTMLString(s);
    
    return null; /* everything is awesome O */
  }
  
  
  /**
   * Renders a java.awt.BufferedImage to the WOResponse of the given context.
   * Remember to configure:<pre>
   *   -Djava.awt.headless=true</pre>
   * (thats the VM arguments of the run panel in Eclipse) 
   * 
   * @param _img   - the BufferedImage object to render
   * @param _ctx - the WOContext to render the image in
   * @return null if everything went fine, an Exception otherwise
   */
  public Exception renderBufferedImage(BufferedImage _img, WOContext _ctx) {
    // TBD: this method could be improved a lot, but it works well enough for
    //      simple cases
    
    if (_img == null)
      return new JoInternalErrorException("got no image to render");
    
    /* find a proper image writer */
    
    String                usedType = null;
    Iterator<ImageWriter> writers;
    ImageWriter           writer = null;
    WORequest             rq = _ctx.request();
    if (rq != null) {
      // TBD: just iterate over the accepted (image/) types (considering
      //      the value quality) and check each
      
      if (rq.acceptsContentType("image/png", false /* direct match */)) {
        if ((writers = ImageIO.getImageWritersByMIMEType("image/png")) != null)
          writer = writers.next();
        if (writer != null)
          usedType = "image/png";
      }
      if (writer == null && rq.acceptsContentType("image/gif", false)) {
        if ((writers = ImageIO.getImageWritersByMIMEType("image/gif")) != null)
          writer = writers.next();
        if (writer != null)
          usedType = "image/gif";
      }
      if (writer == null && rq.acceptsContentType("image/jpeg", false)) {
        if ((writers = ImageIO.getImageWritersByMIMEType("image/jpeg")) != null)
          writer = writers.next();
        if (writer != null)
          usedType = "image/jpeg";
      }
    }
    if (writer == null) {
      if ((writers = ImageIO.getImageWritersByMIMEType("image/png")) != null)
        writer = writers.next();
      if (writer != null)
        usedType = "image/png";
    }
    if (writer == null)
      return new JoInternalErrorException("found no writer for image: " + _img);
    
    
    /* prepare WOResponse */

    WOResponse r = _ctx.response();
    r.setStatus(WOMessage.HTTP_STATUS_OK);
    r.setHeaderForKey("inline", "content-disposition");
    if (usedType != null) r.setHeaderForKey(usedType, "content-type");
    // TBD: do we know the content-length? If not, should we generate to a
    //      buffer to avoid confusing the browser (IE ...)
    r.enableStreaming();
    
    
    /* write */
    
    ImageOutputStream ios = null;
    try {
      ios = ImageIO.createImageOutputStream(rq.outputStream());
    }
    catch (IOException e) {
      log.warn("could not create image output stream: " + _img);
      return e;
    }
    
    writer.setOutput(ios);
    
    try {
      writer.write(null, new IIOImage(_img, null, null), null);
      ios.flush();
      writer.dispose();
      ios.close();
    }
    catch (IOException e) {
      log.warn("failed to write image to stream", e);
      return e;
    }
    
    
    return null; /* everything is awesome O */
  }
  
  /**
   * Renders an arbitary java.awt.Image object by converting it to a 
   * BufferedImage and then calling renderBufferedImage.
   * 
   * @param _img - the java.awt.Image to be rendered
   * @param _ctx - the context to render the Image in
   * @return null if everything went fine, an exception otherwise
   */
  public Exception renderImage(Image _img, WOContext _ctx) {
    /* Not sure whether thats the best way to accomplish this :-) */
    if (_img == null)
      return new JoInternalErrorException("got no image to render");
    
    if (_img instanceof BufferedImage)
      return this.renderBufferedImage((BufferedImage)_img, _ctx);
    
    int width  = _img.getWidth(null);
    int height = _img.getHeight(null);
    
    BufferedImage bi =
      new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    
    Graphics2D g2d = bi.createGraphics();      
    g2d.drawImage(_img, 0, 0, width, height, null);
    
    return this.renderBufferedImage(bi, _ctx);
  }
}
