/*
 * 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.dynamo.velocity;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import org.apache.log4j.Logger;
import org.apache.velocity.*;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.context.AbstractContext;
import org.apache.velocity.context.Context;
import org.apache.velocity.exception.*;
import org.apache.velocity.runtime.*;
import org.w3c.dom.*;
import com.silverwrist.util.xml.*;
import com.silverwrist.dynamo.except.*;
import com.silverwrist.dynamo.iface.*;
import com.silverwrist.dynamo.util.*;

public class VelocityRenderer
    implements NamedObject, TextRenderer, ComponentInitialize, ComponentShutdown, VelocityRendererConfig
{
  /*--------------------------------------------------------------------------------
   * Internal context which implements "tool" objects
   *--------------------------------------------------------------------------------
   */

  private class ToolContext extends AbstractContext
  {
    /*====================================================================
     * Attributes
     *====================================================================
     */

    private StdObject m_std;
    private HashMap m_objects;

    /*====================================================================
     * Constructor
     *====================================================================
     */

    ToolContext(Context inner, TextRenderControl control) throws RenderClassCreationException
    {
      super(inner);
      m_std = new StdObject(control);
      m_objects = new HashMap(m_tool_objects);  // this should copy all objects

      // Create all objects that just have classes.
      Class klass = null;
      Iterator it = m_tool_classes.entrySet().iterator();
      try
      { // put loop inside, for easier operation
	while (it.hasNext())
	{ // loop around and create objects using newInstance
	  Map.Entry ntry = (Map.Entry)(it.next());
	  klass = (Class)(ntry.getValue());
	  m_objects.put(ntry.getKey(),klass.newInstance());

	} // end while

      } // end try
      catch (IllegalAccessException e)
      { // this object could not be created
	throw new RenderClassCreationException(klass,e);

      } // end catch
      catch (InstantiationException e)
      { // same basic result here
	throw new RenderClassCreationException(klass,e);

      } // end catch

      // Create all objects with actual constructors.
      Object[] args = new Object[1];
      args[0] = control;
      Constructor ctor = null;
      it = m_tool_ctors.entrySet().iterator();
      try
      { // put loop inside, for easier operation
	while (it.hasNext())
	{ // loop around and create objects using newInstance
	  Map.Entry ntry = (Map.Entry)(it.next());
	  ctor = (Constructor)(ntry.getValue());
	  m_objects.put(ntry.getKey(),ctor.newInstance(args));

	} // end while

      } // end try
      catch (IllegalAccessException e)
      { // this object could not be created
	throw new RenderClassCreationException(ctor,e);

      } // end catch
      catch (InstantiationException e)
      { // same basic result here
	throw new RenderClassCreationException(ctor,e);

      } // end catch
      catch (IllegalArgumentException e)
      { // same basic result here
	throw new RenderClassCreationException(ctor,e);

      } // end catch
      catch (InvocationTargetException e)
      { // for this exception type, "unwrap" the exception
	throw new RenderClassCreationException(ctor,e.getCause());

      } // end catch

    } // end constructor

    /*====================================================================
     * Abstract implementations from class AbstractContext
     *====================================================================
     */

    public Object internalGet(String key)
    {
      if (key.equals("std"))
	return m_std;
      else if (key.startsWith("std_"))
	return m_objects.get(key.substring(4));
      return null;

    } // end internalGet

    public Object internalPut(String key, Object value)
    {
      Context ctxt = getChainedContext();
      if (ctxt==null)
	return null;
      else
	return ctxt.put(key,value);

    } // end internalPut

    public boolean internalContainsKey(Object key)
    {
      String skey = key.toString();
      if (skey.equals("std"))
	return true;
      else if (skey.startsWith("std_"))
	return m_objects.containsKey(skey.substring(4));
      return false;

    } // end internalContainsKey

    public Object[] internalGetKeys()
    {
      ArrayList rc = new ArrayList();
      rc.add("std");
      Iterator it = m_objects.keySet().iterator();
      while (it.hasNext())
	rc.add("std_" + it.next().toString());
      return rc.toArray();

    } // end internalGetKeys

    public Object internalRemove(Object key)
    {
      Context ctxt = getChainedContext();
      if (ctxt==null)
	return null;
      else
	return ctxt.remove(key);

    } // end internalRemove

  } // end class ToolContext

  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static final String LOGGING_CATEGORY_NAME = "velocity.engine";

  public static final String PREFIX_NAME = "com.silverwrist.dynamo.velocity.ResourcePrefix";

  private static Logger vel_logger = Logger.getLogger(LOGGING_CATEGORY_NAME);
  private static Logger logger = Logger.getLogger(VelocityRenderer.class);

  private static final Class[][] CTOR_PARAM_TYPES =
  {
    { TextRenderControl.class },
    { RenderControl.class     },
    { ServiceProvider.class   }
  };

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

  private VelocityEngine m_ve;                         // the actual Velocity engine
  private String m_name;                               // object name
  private ComponentShutdown m_hook = null;             // called to remove init service hook
  private ComponentShutdown m_removal = null;          // called to remove this from execution
  private ComponentShutdown m_removal2 = null;         // called to remove this from execution
  private Hashtable m_tool_objects = new Hashtable();  // tool objects
  private Hashtable m_tool_classes = new Hashtable();  // tool classes
  private Hashtable m_tool_ctors = new Hashtable();    // tool constructors

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  public VelocityRenderer()
  {
    // Create the engine here and do a partial init of properties.
    m_ve = new VelocityEngine();
    m_ve.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
		     "org.apache.velocity.runtime.log.SimpleLog4JLogSystem");
    m_ve.setProperty("runtime.log.logsystem.log4j.category",LOGGING_CATEGORY_NAME);
    m_ve.setProperty("resource.loader","dynamo");
    m_ve.setProperty("dynamo.loader.description","Dynamo Resource Loader");
    m_ve.setProperty("dynamo.resource.loader.class",VelocityDynamoResourceLoader.class.getName());
    m_ve.setProperty("velocimacro.permissions.allow.inline","true");
    m_ve.setProperty("velocimacro.permissions.allow.inline.to.replace.global","false");
    m_ve.setProperty("velocimacro.permissions.allow.inline.local.scope","true");

  } // end constructor

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

  private static final Constructor getAppropriateConstructor(Class klass)
  {
    for (int i=0; i<CTOR_PARAM_TYPES.length; i++)
    { // The CTOR_PARAM_TYPES array has us look for a constructor that takes a
      // TextRenderControl, then all its superinterfaces, in order.
      try
      { // look for a constructor
	Constructor rc = klass.getConstructor(CTOR_PARAM_TYPES[i]);
	return rc;  // found one - that's good enough

      } // end try
      catch (NoSuchMethodException e)
      { // on error, just continue through to the next loop iteration
      } // end catch

    } // end for

    return null;

  } // end getAppropriateConstructor

  private final boolean nameDefined(String name)
  {
    return (   m_tool_objects.containsKey(name) || m_tool_classes.containsKey(name)
            || m_tool_ctors.containsKey(name));

  } // end nameDefined

  private final void render1(VelocityRenderable vr, TextRenderControl control)
      throws IOException, RenderingException
  {
    try
    { // set up the content type
      control.setContentType(vr.getMimeType());

      // Load the Velocity template.
      Template templ = m_ve.getTemplate(vr.getTemplateName());

      // Create a context containing the arguments.
      VelocityContext ctxt = new VelocityContext(new RenderableContext(vr));

      // Merge the template onto the output.
      Writer wr = control.getWriter();
      templ.merge(new ToolContext(ctxt,control),wr);
      wr.flush();

    } // end try
    catch (ResourceNotFoundException e)
    { // turn this into a RenderingException
      RenderingException re = new RenderingException(VelocityRenderer.class,"VelocityMessages",
						     "velocity.resource",e);
      re.setParameter(0,"/" + vr.getTemplateName());
      throw re;

    } // end catch
    catch (ParseErrorException e)
    { // turn this into a RenderingException
      RenderingException re = new RenderingException(VelocityRenderer.class,"VelocityMessages",
						     "velocity.parseError",e);
      re.setParameter(0,e.getMessage());
      re.setParameter(1,"/" + vr.getTemplateName());
      throw re;

    } // end catch
    catch (MethodInvocationException e)
    { // turn this into a RenderingException too
      Throwable inner = e.getWrappedThrowable();
      RenderingException re = new RenderingException(VelocityRenderer.class,"VelocityMessages",
						     "velocity.methodInvoke",inner);
      re.setParameter(0,inner.getClass().getName());
      re.setParameter(1,e.getMethodName());
      re.setParameter(2,e.getReferenceName());
      re.setParameter(3,"/" + vr.getTemplateName());
      throw re;

    } // end catch
    catch (RenderingException e)
    { // reflect RenderingExceptions onward
      throw e;

    } // end catch
    catch (IOException e)
    { // let IOExceptions pass
      throw e;

    } // end catch
    catch (Exception e)
    { // Velocity can throw some weird shit - wrap it up and fire it out
      logger.error("Caught unhandled exception in VelocityRenderer.render",e);
      RenderingException re = new RenderingException(VelocityRenderer.class,"VelocityMessages",
						     "velocity.unknownExcept",e);
      re.setParameter(0,e.getClass().getName());
      re.setParameter(1,"/" + vr.getTemplateName());
      throw re;

    } // end catch

  } // end render1

  private final void render2(VelocityOwnTemplate vot, TextRenderControl control)
      throws IOException, RenderingException
  {
    try
    { // set up the content type
      control.setContentType(vot.getMimeType());

      // Get the identifier.
      String ident = vot.getIdentifier();
      if (ident==null)
	ident = "VelocityOwnTemplate";

      // Create the context containing the arguments.
      VelocityContext ctxt = new VelocityContext(new RenderableContext(vot));

      // Merge the template onto the output.
      Writer wr = control.getWriter();
      if (!(m_ve.evaluate(new ToolContext(ctxt,control),wr,ident,vot.getTemplate())))
	throw new RenderingException(VelocityRenderer.class,"VelocityMessages","velocity.strTemplateFail");
      wr.flush();

    } // end try
    catch (ResourceNotFoundException e)
    { // turn this into a RenderingException
      throw new RenderingException(VelocityRenderer.class,"VelocityMessages","velocity.resource2",e);

    } // end catch
    catch (ParseErrorException e)
    { // turn this into a RenderingException
      RenderingException re = new RenderingException(VelocityRenderer.class,"VelocityMessages",
						     "velocity.parseError2",e);
      re.setParameter(0,e.getMessage());
      throw re;

    } // end catch
    catch (MethodInvocationException e)
    { // turn this into a RenderingException too
      Throwable inner = e.getWrappedThrowable();
      RenderingException re = new RenderingException(VelocityRenderer.class,"VelocityMessages",
						     "velocity.methodInvoke2",inner);
      re.setParameter(0,inner.getClass().getName());
      re.setParameter(1,e.getMethodName());
      re.setParameter(2,e.getReferenceName());
      throw re;

    } // end catch
    catch (RenderingException e)
    { // reflect RenderingExceptions onward
      throw e;

    } // end catch
    catch (IOException e)
    { // let IOExceptions pass
      throw e;

    } // end catch
    catch (Exception e)
    { // Velocity can throw some weird shit - wrap it up and fire it out
      logger.error("Caught unhandled exception in VelocityRenderer.render",e);
      RenderingException re = new RenderingException(VelocityRenderer.class,"VelocityMessages",
						     "velocity.unknownExcept2",e);
      re.setParameter(0,e.getClass().getName());
      throw re;

    } // end catch

  } // end render2

  /*--------------------------------------------------------------------------------
   * Implementations from interface NamedObject
   *--------------------------------------------------------------------------------
   */

  public String getName()
  {
    return m_name;

  } // end getName

  /*--------------------------------------------------------------------------------
   * Implementations from interface TextRenderer
   *--------------------------------------------------------------------------------
   */

  public void render(Object obj, TextRenderControl control) throws IOException, RenderingException
  {
    if (obj==null)
      throw new NullPointerException("VelocityRenderer got null object to render");

    if (obj instanceof VelocityRenderable)
    { // render the object!
      render1((VelocityRenderable)obj,control);
      return;

    } // end if

    if (obj instanceof VelocityOwnTemplate)
    { // render the object!
      render2((VelocityOwnTemplate)obj,control);
      return;

    } // end if

    // Can't render this object - throw an exception
    RenderingException re = new RenderingException(VelocityRenderer.class,"VelocityMessages",
						   "velocity.noRender");
    re.setParameter(0,obj.getClass().getName());
    throw re;
    
  } // end render

  /*--------------------------------------------------------------------------------
   * Implementations from interface ComponentInitialize
   *--------------------------------------------------------------------------------
   */

  public void initialize(Element config_root, ServiceProvider services) throws ConfigException
  {
    XMLLoader loader = XMLLoader.get();
    try
    { // get the object's name
      m_name = loader.getAttribute(config_root,"name");

      // Get the resource prefix.
      DOMElementHelper config_h = new DOMElementHelper(config_root);
      String s = config_h.getSubElementText("resource-prefix");
      if (s==null)
      { // set up default prefix
	logger.debug("no resource-prefix attribute - defaulting");
	s = "/";

      } // end if
      else
      { // make sure the prefix is properly set
	if (!(s.startsWith("/")))
	  s = "/" + s;
	if (!(s.endsWith("/")))
	  s += "/";

      } // end else

      m_ve.setApplicationAttribute(PREFIX_NAME,s);

      // Load the standard object list.
      List l = loader.getMatchingSubElements(config_root,"std-object");
      Iterator it = l.iterator();
      while (it.hasNext())
      { // load up all the attributes for each standard object
	Element elt = (Element)(it.next());
	String name = loader.getAttribute(elt,"name");
	String klassname = loader.getAttribute(elt,"classname");
	boolean create = loader.getAttributeBoolean(elt,"create",false);

	// get the right object class
	Class klass = null;
	try
	{ // load the class
	  klass = Class.forName(klassname);

	} // end try
	catch (ClassNotFoundException cnfe)
	{ // throw a ConfigException here
	  ConfigException ce = new ConfigException(VelocityRenderer.class,"VelocityMessages",
						   "stdObj.classNotFound");
	  ce.setParameter(0,klassname);
	  throw ce;

	} // end catch

	// Do we create it now, or later?
	if (create)
	{ // We create this object at render time.  It may have a constructor that allows
	  // it to use the TextRenderContext.  Find it if it's there.
	  Constructor ctor = getAppropriateConstructor(klass);
	  if (ctor==null)
	    m_tool_classes.put(name,klass);
	  else
	    m_tool_ctors.put(name,ctor);

	} // end if
	else
	{ // create the object instance now and save it for later
	  try
	  { // create instance and add it to the object list
	    m_tool_objects.put(name,klass.newInstance());

	  } // end try
	  catch (IllegalAccessException e)
	  { // this object could not be created
	    ConfigException ce = new ConfigException(VelocityRenderer.class,"VelocityMessages",
						     "stdObj.createFail",e);
	    ce.setParameter(0,klassname);
	    throw ce;

	  } // end catch
	  catch (InstantiationException e)
	  { // same basic result here
	    ConfigException ce = new ConfigException(VelocityRenderer.class,"VelocityMessages",
						     "stdObj.createFail",e);
	    ce.setParameter(0,klassname);
	    throw ce;

	  } // end catch
	  
	} // end else

      } // end while

    } // end try
    catch (XMLLoadException e)
    { // XML format error, bail out
      throw new ConfigException(e);

    } // end catch

    // Get Dynamo's ResourceProvider and put it where our ResourceLoader can find it.
    ResourceProvider prov = (ResourceProvider)(services.queryService(ResourceProvider.class));
    m_ve.setApplicationAttribute(ResourceProvider.class.getName(),prov);

    // We're all set now, initialize Velocity!
    try
    { // call the init method
      m_ve.init();

    } // end try
    catch (Exception e)
    { // whoops!  the engine failed!
      ConfigException ce = new ConfigException(VelocityRenderer.class,
					       "VelocityMessages","velocityInit.failure",e);
      ce.setParameter(0,e.getMessage());
      throw ce;

    } // end catch

    // Hook the initialization services stack to allow new objects to be configured.
    HookServiceProviders hooker = (HookServiceProviders)(services.queryService(HookServiceProviders.class));
    SingletonServiceProvider ssp = new SingletonServiceProvider("VelocityRenderer",
								VelocityRendererConfig.class,
								(VelocityRendererConfig)this);
    m_hook = hooker.hookInitServiceProvider(ssp);

    // Register this object as a renderer for our object interfaces.
    RendererRegistration rr = (RendererRegistration)(services.queryService(RendererRegistration.class));
    m_removal = rr.registerRenderer(VelocityRenderable.class,this);
    m_removal2 = rr.registerRenderer(VelocityOwnTemplate.class,this);

  } // end initialize

  /*--------------------------------------------------------------------------------
   * Implementations from interface ComponentShutdown
   *--------------------------------------------------------------------------------
   */

  public void shutdown()
  {
    m_removal2.shutdown();
    m_removal2 = null;
    m_removal.shutdown();
    m_removal = null;
    m_hook.shutdown();
    m_hook = null;
    m_ve = null;
    m_tool_objects.clear();
    m_tool_classes.clear();
    m_tool_ctors.clear();

  } // end shutdown

  /*--------------------------------------------------------------------------------
   * Implementations from interface VelocityRendererConfig
   *--------------------------------------------------------------------------------
   */

  public ComponentShutdown addStandardComponentInstance(String name, Object obj) throws ConfigException
  {
    if (logger.isDebugEnabled())
      logger.debug("Defining new component instance \"" + name + "\" in Velocity renderer");
    if (nameDefined(name))
    { // name is already defined!
      ConfigException ce = new ConfigException(VelocityRenderer.class,"VelocityMessages","obj.nameDefined");
      ce.setParameter(0,name);
      throw ce;

    } // end if

    m_tool_objects.put(name,obj);
    return new ShutdownHashtableRemove(m_tool_objects,name);

  } // end addStandardComponentInstance

  public ComponentShutdown addStandardComponentClass(String name, Class klass) throws ConfigException
  {
    if (logger.isDebugEnabled())
      logger.debug("Defining new component class \"" + name + "\" in Velocity renderer");
    if (nameDefined(name))
    { // name is already defined!
      ConfigException ce = new ConfigException(VelocityRenderer.class,"VelocityMessages","obj.nameDefined");
      ce.setParameter(0,name);
      throw ce;

    } // end if

    Constructor ctor = getAppropriateConstructor(klass);
    if (ctor==null)
    { // add class to classes Hashtable
      m_tool_classes.put(name,klass);
      return new ShutdownHashtableRemove(m_tool_classes,name);

    } // end if
    else
    { // add constructor to constructors Hashtable
      m_tool_ctors.put(name,ctor);
      return new ShutdownHashtableRemove(m_tool_ctors,name);

    } // end else

  } // end addStandardComponentClass

} // end class VelocityRenderer
