/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.jsp.java;

import com.caucho.jsp.*;
import com.caucho.config.*;
import com.caucho.util.*;
import com.caucho.xml.QName;
import com.caucho.vfs.*;

import javax.servlet.jsp.tagext.*;
import javax.faces.component.*;
import javax.faces.event.*;
import javax.el.*;
import java.lang.reflect.*;
import java.util.*;
import java.io.*;

/**
 * Represents a custom tag.
 */
public class JsfTagNode extends JsfNode
{
  private JspNode _next;
  private Class _componentClass;

  private String _facetName;

  private Attr _idAttr;
  private String _prevJsfId;

  private Attr _bindingAttr;

  private ArrayList<Attr> _attrList = new ArrayList<Attr>();

  public void setComponentClass(Class cl)
  {
    _componentClass = cl;
  }

  public void setNext(JspNode next)
  {
    _next = next;
  }

  /**
   * Adds an attribute.
   */
  public void addAttribute(QName qName, String value)
    throws JspParseException
  {
    String name = qName.getName();

    String setterName = ("set" + Character.toUpperCase(name.charAt(0))
			 + name.substring(1));

    if (name.equals("action")
	&& ActionSource2.class.isAssignableFrom(_componentClass))
      setterName = "setActionExpression";
    else if (name.equals("actionListener")
	     && ActionSource2.class.isAssignableFrom(_componentClass))
      setterName = "addActionListener";
    else if (name.equals("escape") &&
             UISelectItem.class.isAssignableFrom(_componentClass))
      setterName = "setItemEscaped";

    Method method = findSetter(_componentClass, setterName);

    if (method != null) {
      _attrList.add(new Attr(name, method, value));
    }
    else if (name.equals("binding")) {
      if (! value.startsWith("#{"))
	throw error(L.l("JSF binding attribute requires a deferred value at '{0}'",
			value));
	
      Attr attr = new Attr(name, method, value);

      _bindingAttr = attr;
      _attrList.add(attr);
    }
    else {
      super.addAttribute(qName, value);
    }
  }

  /**
   * Adds a JspAttribute attribute.
   *
   * @param qName the name of the attribute.
   * @param value the value of the attribute.
   */
  public void addAttribute(QName qName, JspAttribute value)
    throws JspParseException
  {
    if (value.isStatic()) {
      addAttribute(qName, value.getStaticText().trim());
    }
    else {
      String name = qName.getName();

      String setterName = ("set" + Character.toUpperCase(name.charAt(0))
			   + name.substring(1));

      Method method = findSetter(_componentClass, setterName);

      if (method != null) {
	_attrList.add(new Attr(name, method, value));
      }
      else {
	super.addAttribute(qName, value);
	return;
      }
    }
  }

  /**
   * Called after all the attributes from the tag.
   */
  public void endAttributes()
    throws JspParseException
  {
    if (_parent instanceof JsfFacetNode) {
      _facetName = ((JsfFacetNode) _parent).getName();
    }
    else if (_parent instanceof CustomTag) {
      final QName qName = _parent.getQName();

      if ((qName.getNamespaceURI().indexOf("http://java.sun.com/jsf/core") > -1)
	  && "facet".equals(qName.getLocalName())) {

	final Object facetName = ((CustomTag) _parent).getAttribute("name");
	
	if (facetName instanceof String) {
	  _facetName = facetName.toString();
	}
      }
    }

    _idAttr = findAttribute("id");

    if (_idAttr != null)
      addJsfId(_parent);
    else if (isJsfIdRequired())
      addJsfId(this);

    if (_idAttr == null) {
      _prevJsfId = getPrevJsfId();
    }
  }

  public void endElement()
  {
    if (isJsfParentRequired()) {
      addJsfId(this);
    }
  }

  String getJsfId()
  {
    if (_idAttr != null)
      return _idAttr.getValue();
    else
      return null;
  }

  private boolean isJsfIdRequired()
  {
    if (EditableValueHolder.class.isAssignableFrom(_componentClass))
      return true;
    else if (ActionSource.class.isAssignableFrom(_componentClass))
      return true;
    else if (ActionSource2.class.isAssignableFrom(_componentClass))
      return true;
    else if (_bindingAttr != null)
      return true;
    else if (UISelectItem.class.isAssignableFrom(_componentClass))
      return true;
    else if (UISelectItems.class.isAssignableFrom(_componentClass))
      return true;
    
    JspNode parent = _parent;

    ArrayList<JspNode> children = parent.getChildren();

    boolean isJsfIdRequired = false;
      
    for (int i = 0; i < children.size(); i++) {
      JspNode child = children.get(i);
	
      if (child == this)
	return isJsfIdRequired;
      else if (child instanceof JsfTagNode) {
	JsfTagNode jsfNode = (JsfTagNode) child;
	String id = jsfNode.getJsfId();

	if (id != null)
	  isJsfIdRequired = false;
      }
      else if (child.isJsfParentRequired())
	isJsfIdRequired = true;
    }

    return false;
  }
  
  private String getPrevJsfId()
  {
    JspNode parent = _parent;

    ArrayList<JspNode> children = parent.getChildren();

    if (children != null) {
      boolean isFound = false;
      
      for (int i = children.size() - 1; i >= 0; i--) {
	JspNode child = children.get(i);
	
	if (child == this)
	  isFound = true;
	else if (isFound && child instanceof JsfTagNode) {
	  JsfTagNode jsfNode = (JsfTagNode) child;
	  String id = jsfNode.getJsfId();

	  if (id != null)
	    return id;
	}
      }
    }

    return null;
  }

  private static void addJsfId(JspNode node)
  {
    if (node == null)
      return;

    addJsfId(node.getParent());

    if (node instanceof JsfTagNode) {
      JsfTagNode jsfNode = (JsfTagNode) node;

      if (jsfNode._idAttr != null)
	return;
      
      jsfNode._idAttr = jsfNode.findAttribute("id");

      if (jsfNode._idAttr == null) {
	Method method = jsfNode.findSetter(jsfNode._componentClass, "setId");

	if (method == null)
	  throw new IllegalStateException(jsfNode._componentClass + " id");
      
	String id = "j_id_" + jsfNode._gen.generateJspId();
	
	jsfNode._idAttr = new Attr("id", method, id);

	jsfNode._attrList.add(jsfNode._idAttr);
      }
    }
  }

  private Attr findAttribute(String name)
  {
    for (int i = 0; i < _attrList.size(); i++) {
      Attr attr = _attrList.get(i);
      
      if (attr.getName().equals(name))
	return attr;
    }

    return null;
  }

  /**
   * Generates the XML text representation for the tag validation.
   *
   * @param os write stream to the generated XML.
   */
  public void printXml(WriteStream os)
    throws IOException
  {
    if (_next != null)
      _next.printXml(os);
  }
  
  /**
   * generates prologue data.
   */
  @Override
  public void generatePrologue(JspJavaWriter out)
    throws Exception
  {
    _var = "_jsp_comp" + _gen.uniqueId();
    
    super.generatePrologue(out);
  }
  
  /**
   * Generates the code for a custom tag.
   *
   * @param out the output writer for the generated java.
   */
  public void generate(JspJavaWriter out)
    throws Exception
  {
    String contextVar = "null";

    if (_gen.isJsfPrologueInit())
      contextVar = "_jsp_faces_context";
    
    String parentVar = _parent.getJsfVar();

    String parentBodyVar = _parent.getJsfBodyVar();

    String prevId;

    String oldParentVar = null;

    String bindingVar = null;

    if (isJsfParentRequired()) {
      oldParentVar = "_jsp_jsf_parent_" + _gen.uniqueId();

      out.println("Object " + oldParentVar
		  + " = request.getAttribute(\"caucho.jsf.parent\");");
    }

    if (_prevJsfId != null)
      prevId = "\"" + _prevJsfId + "\"";
    else
      prevId = null;

    if (parentBodyVar != null) {
      out.println("com.caucho.jsp.jsf.JsfTagUtil.addVerbatim("
		  + parentVar
		  + ", " + prevId
		  + ", " + parentBodyVar + ");");
    }

    if (! hasBodyContent()) {
    }
    else if (parentBodyVar != null)
      _bodyVar = parentBodyVar;
    else {
      _bodyVar = "_jsp_body" + _gen.uniqueId();

      out.println("com.caucho.jsp.BodyContentImpl " + _bodyVar
		  + " = (com.caucho.jsp.BodyContentImpl) pageContext.pushBody();");
      out.println("out =  " + _bodyVar + ";");
    }

    String className;

    if (_bindingAttr != null)
      className = UIComponent.class.getName();
    else
      className = _componentClass.getName();

    out.println(className + " " + _var + ";");

    if (_bindingAttr != null) {
      bindingVar = ("_caucho_value_expr_" +
		    _gen.addValueExpr(_bindingAttr.getValue(),
				      UIComponent.class.getName()));
    }

    if (_facetName != null) {
      out.print(_var + " = (" + className + ")");
      out.println(" com.caucho.jsp.jsf.JsfTagUtil.findFacet("
		  + contextVar
		  + ", request"
		  + ", " + parentVar
		  + ", \"" + _facetName + "\");");

      out.println("if (" + _var + " == null) {");
      out.pushDepth();
      
      out.print(_var + " = (" + className + ")");
      out.println(" com.caucho.jsp.jsf.JsfTagUtil.addFacet("
		  + contextVar
		  + ", request"
		  + ", " + parentVar
		  + ", \"" + _facetName + "\""
		  + ", " + bindingVar
		  + ", " + _componentClass.getName() + ".class);");
    }
    else if (_idAttr != null) {
      out.print(_var + " = (" + className + ")");
      out.println(" com.caucho.jsp.jsf.JsfTagUtil.findPersistent("
		  + contextVar
		  + ", request"
		  + ", " + parentVar
		  + ", \"" + _idAttr.getValue() + "\");");

      out.println("if (" + _var + " == null) {");
      out.pushDepth();
      
      out.print(_var + " = (" + className + ")");
      out.println(" com.caucho.jsp.jsf.JsfTagUtil.addPersistent("
		  + contextVar
		  + ", request"
		  + ", " + parentVar
		  + ", " + bindingVar
		  + ", " + _componentClass.getName() + ".class);");
    }
    else {
      out.print(_var + " = (" + className + ")");

      out.println(" com.caucho.jsp.jsf.JsfTagUtil.addTransient("
		  + contextVar
		  + ", request"
		  + ", " + parentVar
		  + ", " + prevId
		  + ", " + _componentClass.getName() + ".class);");
    }

    for (int i = 0; i < _attrList.size(); i++) {
      Attr attr = (Attr) _attrList.get(i);

      Method method = attr.getMethod();
      Class type = null;

      if (method != null)
	type = method.getParameterTypes()[0];
      else if (attr.getName().equals("binding"))
	type = UIComponent.class;
      
      JspAttribute jspAttr = attr.getAttr();
      String value = attr.getValue();

      if (jspAttr != null) {
	generateSetParameter(out, _var, jspAttr, method,
			     true, null, false, null);
      }
      else if (ActionListener.class.isAssignableFrom(type)) {
	String exprVar = "_caucho_method_expr_" + _gen.addMethodExpr(value, "void foo(javax.faces.event.ActionEvent)");
	
	out.println(_var + ".addActionListener(new javax.faces.event.MethodExpressionActionListener(" + exprVar + "));");
      }
      else if ("validator".equals(attr.getName())
	       && UIInput.class.isAssignableFrom(_componentClass)) {
	out.print("((javax.faces.component.UIInput)" + _var + ").addValidator(new javax.faces.validator.MethodExpressionValidator(");

	String signature
	  = "void foo(javax.faces.context.FacesContext, javax.faces.component.UIComponent, java.lang.Object)";

	String exprVar = "_caucho_method_expr_" +
			 _gen.addMethodExpr(value, signature);

	out.println(exprVar + "));");
      }
      else if ("valueChangeListener".equals(attr.getName())
	       && UIInput.class.isAssignableFrom(_componentClass)) {
	out.print("((javax.faces.component.UIInput)" + _var + ").addValueChangeListener(new javax.faces.event.MethodExpressionValueChangeListener(");

	String signature = "void foo(javax.faces.event.ValueChangeEvent)";

	String exprVar = "_caucho_method_expr_" +
			 _gen.addMethodExpr(value, signature);

	out.println(exprVar + "));");
      }
      else if (_bindingAttr != null && ! "id".equals(attr.getName())
	       || (value.indexOf("#{") >= 0
		   && ! ValueExpression.class.isAssignableFrom(type)
		   && ! MethodExpression.class.isAssignableFrom(type))) {
	// jsf/3153
	out.print(_var + ".setValueExpression(\"" + attr.getName() + "\", ");

	String exprVar = "_caucho_value_expr_" + _gen.addValueExpr(value, type.getName());
	
	out.println(exprVar + ");");
      }
      else {
	out.print(_var + "." + method.getName() + "(");

	out.print(generateParameterValue(type, value, true, null, false));
	
	out.println(");");
      }
    }

    if (_idAttr != null || _facetName != null) {
      if (oldParentVar != null)
	out.println("request.setAttribute(\"caucho.jsf.parent\""
		    + ", new com.caucho.jsp.jsf.JsfComponentTag("
		    + _var + ", true, " + _bodyVar + "));");
      
      out.popDepth();
      out.println("}");

      if (oldParentVar != null) {
	out.println("else");
	out.println("  request.setAttribute(\"caucho.jsf.parent\""
		    + ", new com.caucho.jsp.jsf.JsfComponentTag("
		    + _var + ", false, " + _bodyVar + "));");
      }
    }

    generateChildren(out);

    if (oldParentVar != null) {
      out.println("request.setAttribute(\"caucho.jsf.parent\", "
		  + oldParentVar + ");");
    }

    if (_bodyVar != null && parentBodyVar == null) {
      out.println("out = pageContext.popBody();");
      out.println("pageContext.releaseBody(" + _bodyVar + ");");
    }
  }
}
