package org.jboss.seam.ui.facelet;

import static org.jboss.seam.ScopeType.STATELESS;
import static org.jboss.seam.annotations.Install.BUILT_IN;

import java.io.IOException;
import java.io.StringWriter;
import java.net.URL;

import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

import org.jboss.seam.annotations.AutoCreate;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.core.ResourceLoader;
import org.jboss.seam.faces.Renderer;
import org.jboss.seam.mock.MockExternalContext;
import org.jboss.seam.mock.MockFacesContext;
import org.jboss.seam.ui.util.JSF;

import com.sun.facelets.Facelet;
import com.sun.facelets.impl.DefaultFaceletFactory;
import com.sun.facelets.impl.DefaultResourceResolver;

/**
 * 
 * Implementation of Renderer using Facelets
 * 
 * Especially useful for sending email using Seam Mail
 * 
 * @author Pete Muir
 * @author Norman Richards
 *
 */

@Scope(STATELESS)
@BypassInterceptors
@Name("org.jboss.seam.faces.renderer")
@AutoCreate
@Install(value = true, precedence = BUILT_IN, classDependencies="com.sun.facelets.Facelet")
public class FaceletsRenderer extends Renderer
{
   /**
    * Sets up and tears down the rendering (FacesContext, ExternalContext) using 
    * Seam's mock contexts if JSF is not currently running.  Also switches out the
    * ViewRoot for the duration.  Any output written to the ResponseWriter is
    * buffered.
    * 
    * @author Pete Muir
    *
    */
   protected abstract class RenderingContext 
   {
      
      public RenderingContext(String viewId)
      {
         this.viewId = viewId;
      }
      
      private String viewId;
      
      private ClassLoader originalClassLoader;
      private ResponseWriter originalResponseWriter;
      private UIViewRoot originalViewRoot;
      private StringWriter writer = new StringWriter();
      
      /**
       * Call to execute the process method inside the RenderingContext
       */
      public void run() 
      {
         try
         {
            init();
            process();
         }
         finally
         {
            cleanup();
         }
      }
      
      private void init()
      {
         // Make sure we are using the correct ClassLoader
         originalClassLoader = Thread.currentThread().getContextClassLoader();
         Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
         
         // If a FacesContext isn't available, set one up
         if (FacesContext.getCurrentInstance() == null)
         {
            MockFacesContext mockFacesContext = new MockFacesContext(new MockExternalContext())
                     .setCurrent();
            mockFacesContext.createViewRoot();
         }
         
         FacesContext facesContext = FacesContext.getCurrentInstance();
         
         // Wrap the ResponseWriter
         originalResponseWriter = facesContext.getResponseWriter();
         facesContext.setResponseWriter(facesContext.getRenderKit().createResponseWriter(writer,
                  null, null));
         
         // Create a new UIViewRoot
         originalViewRoot = facesContext.getViewRoot();
         UIViewRoot viewRoot = new UIViewRoot();
         viewRoot.setRenderKitId(facesContext.getApplication().getViewHandler().calculateRenderKitId(facesContext));
         viewRoot.setViewId(viewId);
         viewRoot.setLocale(originalViewRoot.getLocale());
         facesContext.setViewRoot(viewRoot);
      }
      
      private void cleanup()
      {
         FacesContext facesContext = FacesContext.getCurrentInstance();
         if (originalResponseWriter != null)
         {
            facesContext.setResponseWriter(originalResponseWriter);
         }
         if (originalViewRoot != null)
         {
            facesContext.setViewRoot(originalViewRoot);
         }
         Thread.currentThread().setContextClassLoader(originalClassLoader);       
      }
      
      /**
       * Return any output written to the ResponseWriter as a String
       */
      public String getOutput() 
      {
         return writer.getBuffer().toString(); 
      }
      
      /**
       * Override to execute within a RenderingContext
       */
      protected abstract void process();
      
   }
   
   /**
    * Render the viewId, anything written to the JSF ResponseWriter is
    * returned
    */
   @Override
   public String render(final String viewId)
   {
      RenderingContext context = new RenderingContext(viewId) {
         
         @Override
         public void process()
         {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            try
            {
               renderFacelet(facesContext, faceletForURL(resourceURL(viewId)));
            }
            catch (IOException e)
            {
               throw new RuntimeException("Error rendering view", e);
            }
         }
         
      };
      context.run();
      
      return context.getOutput();
   }

   /**
    * Get a viewId as a URL
    */
   protected URL resourceURL(String viewId)
   {

      URL url = ResourceLoader.instance().getResource(viewId);

      if (url == null)
      {
         throw new IllegalArgumentException("resource doesn't exist: " + viewId);
      }

      return url;
   }

   /**
    * Get a Facelet for a URL
    */
   protected Facelet faceletForURL(URL url) throws IOException
   {
      return new DefaultFaceletFactory(FaceletCompiler.instance(), new DefaultResourceResolver())
               .getFacelet(url);
   }

   /**
    * Render a Facelet
    */
   protected void renderFacelet(FacesContext facesContext, Facelet facelet) throws IOException
   {
      UIViewRoot root = facesContext.getViewRoot();
      facelet.apply(facesContext, root);
      JSF.renderChildren(facesContext, root);  
   }
   
}
