/*
  Copyright (C) 2007-2008 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.IWOComponentDefinition;
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.WOResourceManager;
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 */);
    try {
      // TBD: shouldn't we call WOApplication appendToResponse?!
      _page.appendToResponse(r, _ctx);
    }
    finally { /* ensure that the component stack is OK */
      _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);
  }
  
  
  /* rendering with frames */
  
  /**
   * Renders an object using another 'Frame' object. Usually the object will be
   * a WOComponent and the 'Frame' will be an OFS object representing a
   * WOComponent.
   * <p>
   * If this is the case the method will trigger the renderComponentWithFrame()
   * method.
   * 
   * @param _object - the object to render with the frame
   * @param _frame  - the frame used to render the _object
   * @param _ctx    - the context in which the rendering happens
   * @return null if everything went fine, an Exception otherwise
   */
  public Exception renderObjectWithFrame
    (Object _object, IJoComponentDefinition _frame, WOContext _ctx)
  {
    if (_frame == null)
      return this.renderObjectInContext(_object, _ctx);

    
    /* instantiate frame */
    
    // TBD: maybe we should just use app.pageWithName()? This does not guarantee
    //      that our cdef is triggered, but the RMs will cache our cdef
    WOResourceManager rm = null;
    
    if (_object instanceof WOComponent)
      rm = ((WOComponent)_object).resourceManager();
    
    if (rm == null) {
      // TBD: this is flaky, can get confused wrt JoLookupRM ...
      WOComponent cursor = null;
      if (_ctx != null)
        cursor = _ctx.component();
      if (cursor == null && _ctx != null)
        cursor = _ctx.page();
      if (cursor != null)
        rm = cursor.resourceManager();
    }
    if (rm == null && _ctx != null)
      rm = _ctx.application().resourceManager();
    
    if (rm == null) {
      log.warn("found no resource manager in context: " + _ctx);
      return new JoInternalErrorException("Frame: missing resource manager");
    }

    IWOComponentDefinition cdef =
      _frame.definitionForComponent(null /* name */, null /* langs */, rm);
    if (cdef == null) {
      log.warn("got no cdef for Frame: " + _frame);
      return new JoInternalErrorException("Frame: missing cdef");
    }
    
    //System.err.println("GOT FRAME CDEF:" + cdef);
    
    WOComponent frame = cdef.instantiateComponent(rm, _ctx);
    if (frame == null) {
      log.warn("could not instantiate Frame: " + _frame);
      return new JoInternalErrorException("Frame: could not instantiate");
    }
    
    /* Check whether we embed a component or whether we render an arbitary
     * object.
     */
    if (!(_object instanceof WOComponent)) {
      /* WOComponent itself is also an IJoRenderer :-) */
      // Note: do we first need to check whether the component CAN render the
      //       object?
      return frame.renderObjectInContext(_object, _ctx);
    }
    
    /* OK, embed component */
    WOComponent page = (WOComponent)_object;
    return this.renderComponentWithFrame(page, frame, _ctx);
  }
  
  public Exception renderObjectWithFrame
    (Object _object, IWOComponentDefinition _frame, WOContext _ctx)
  {
    if (_frame == null)
      return this.renderObjectInContext(_object, _ctx);

    
    /* instantiate frame */
    
    // TBD: maybe we should just use app.pageWithName()? This does not guarantee
    //      that our cdef is triggered, but the RMs will cache our cdef
    WOResourceManager rm = null;
    
    if (_object instanceof WOComponent)
      rm = ((WOComponent)_object).resourceManager();
    
    if (rm == null) {
      // TBD: this is flaky, can get confused wrt JoLookupRM ...
      WOComponent cursor = null;
      if (_ctx != null)
        cursor = _ctx.component();
      if (cursor == null && _ctx != null)
        cursor = _ctx.page();
      if (cursor != null)
        rm = cursor.resourceManager();
    }
    if (rm == null && _ctx != null)
      rm = _ctx.application().resourceManager();
    
    if (rm == null) {
      log.warn("found no resource manager in context: " + _ctx);
      return new JoInternalErrorException("Frame: missing resource manager");
    }

    //System.err.println("GOT FRAME CDEF:" + cdef);
    
    WOComponent frame = _frame.instantiateComponent(rm, _ctx);
    if (frame == null) {
      log.warn("could not instantiate Frame: " + _frame);
      return new JoInternalErrorException("Frame: could not instantiate");
    }
    
    /* Check whether we embed a component or whether we render an arbitary
     * object.
     */
    if (!(_object instanceof WOComponent)) {
      /* WOComponent itself is also an IJoRenderer :-) */
      // Note: do we first need to check whether the component CAN render the
      //       object?
      return frame.renderObjectInContext(_object, _ctx);
    }
    
    /* OK, embed component */
    WOComponent page = (WOComponent)_object;
    return this.renderComponentWithFrame(page, frame, _ctx);
  }

  /**
   * Renders a component within another 'Frame' component. On the page stack
   * the Frame is pushed a subcomponent of the component. It gets the template
   * of the component as the 'component content', which it then can embed using
   * the &lt;#WOComponentContent/&gt; element.
   * 
   * @param _page  - the page to embed in a frame
   * @param _frame - the frame to put around the page
   * @param _ctx   - the context in which the rendering happens
   * @return null if everything went fine, an Exception otherwise
   */
  public Exception renderComponentWithFrame
    (WOComponent _page, WOComponent _frame, WOContext _ctx)
  {
    if (_frame == null)
      return this.renderObjectInContext(_page, _ctx);
    
    /* First push page to stack, then push the Frame, then call appendToResponse
     * on the frame.
     * Note that we never call appendToResponse() on the page in this setup!
     */
    if (log.isDebugEnabled()) log.debug("delivering page: " + _page);

    _ctx.setPage(_page);
    _page.ensureAwakeInContext(_ctx);
    _ctx.enterComponent(_page, null /* component-content */);
    try {
      _frame.ensureAwakeInContext(_ctx);
      _ctx.enterComponent(_frame, _page.template());
      try {
        // Note: we do not call the page appendToResponse()
        _frame.appendToResponse(_ctx.response(), _ctx);
      }
      finally {
        _ctx.leaveComponent(_frame);
      }
    }
    finally {
      _ctx.leaveComponent(_page);
    }
    
    return null /* everything OK */;
  }
}
