/*
 * Copyright (c) 1998-2006 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 java.io.*;
import java.util.*;

import java.lang.reflect.Method;

import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;

import com.caucho.vfs.*;
import com.caucho.util.*;

import com.caucho.xml.QName;

import com.caucho.jsp.*;

/**
 * Represents a custom tag.
 */
public class CustomTag extends GenericTag {
  protected String _bodyContent;
  
  /**
   * Generates the code for a custom tag.
   *
   * @param out the output writer for the generated java.
   */
  public void generate(JspJavaWriter out)
    throws Exception
  {
    String name = _tag.getId();
    String className = _tagInfo.getTagClassName();
    String tagHackVar = "_jsp_endTagHack" + _gen.uniqueId();
    Class cl = _tagClass;

    AnalyzedTag analyzedTag = _tag.getAnalyzedTag();

    boolean isIterator = (IterationTag.class.isAssignableFrom(cl) ||
                          BodyTag.class.isAssignableFrom(cl));
    boolean isBodyTag = BodyTag.class.isAssignableFrom(cl);
    boolean isCatch = TryCatchFinally.class.isAssignableFrom(cl);

    boolean isEmpty = isEmpty();
    boolean usesTagBody = isBodyTag && ! isEmpty &&
      analyzedTag.getStartReturnsBuffered();
    boolean hasEndTag = analyzedTag.getDoEnd();
    
    if ("empty".equalsIgnoreCase(_bodyContent)) {
      if (! isEmpty)
	throw error(L.l("<{0}> expects an empty body", getTagName()));
    }
    if (usesTagBody && hasEndTag)
      out.println("com.caucho.jsp.BodyContentImpl " + tagHackVar + " = null;");
    else
      tagHackVar = "out";

    if (! isDeclared()) {
      out.println("if (" + name + " == null) {");
      out.pushDepth();
      generateTagInit(out);
      out.popDepth();
      out.println("}");
      out.println();
    }

    fillAttributes(out, name);

    printVarDeclare(out, VariableInfo.AT_BEGIN);

    String oldTag = "_jsp_writer" + _gen.uniqueId();
    
    if (analyzedTag.getDoCatch())
      out.println("javax.servlet.jsp.JspWriter " + oldTag + " = out;");
    
    if (analyzedTag.getDoCatch() || analyzedTag.getDoFinally()) {
      out.println("try {");
      out.pushDepth();
    }

    boolean hasStartTag = analyzedTag.getDoStart();
    int startCount = ((analyzedTag.getStartReturnsSkip() ? 1 : 0) +
		      (analyzedTag.getStartReturnsInclude() ? 1 : 0) + 
		      (analyzedTag.getStartReturnsBuffered() ? 1 : 0));
    
    int thisId = _gen.uniqueId();
    if (! hasStartTag) {
    }
    else if (startCount == 1) {
      out.println(name + ".doStartTag();");
    }
    else {
      out.println("int _jspEval" + thisId + " = " + name + ".doStartTag();");
    }
    printVarAssign(out, VariableInfo.AT_BEGIN);

    if (analyzedTag.getStartReturnsSkip() &&
	! analyzedTag.getStartReturnsInclude() &&
	! analyzedTag.getStartReturnsBuffered()) {
      // jsp/18cp
      generateChildrenEmpty();
    }
    else if (isEmpty) {
      // jsp/18kc
      /*
      if (isBodyTag)
	out.println("  " + name + ".setBodyContent((javax.servlet.jsp.tagext.BodyContent) null);");
      */
    }
    else {
      if (startCount > 1 && analyzedTag.getStartReturnsSkip()) {
        out.println("if (_jspEval" + thisId + " != javax.servlet.jsp.tagext.Tag.SKIP_BODY) {");
        out.pushDepth();
      }
      else if ((hasVarDeclaration(VariableInfo.NESTED) ||
		childHasScriptlet()) &&
	       ! (analyzedTag.getDoCatch() ||
		  analyzedTag.getDoFinally() ||
		  analyzedTag.getDoAfter() &&
		  analyzedTag.getAfterReturnsAgain())) {
	out.println("{");
	out.pushDepth();
      }

      if (usesTagBody) {
        if (analyzedTag.getStartReturnsBuffered() &&
	    analyzedTag.getStartReturnsInclude()) {
          out.println("if (_jspEval" + thisId + " == javax.servlet.jsp.tagext.BodyTag.EVAL_BODY_BUFFERED) {");
          out.pushDepth();
        }
        
        out.println("out = pageContext.pushBody();");

	if (hasEndTag) {
	  out.println(tagHackVar + " = (com.caucho.jsp.BodyContentImpl) out;");
	  out.println(name + ".setBodyContent(" + tagHackVar + ");");
	}
	else
	  out.println(name + ".setBodyContent((javax.servlet.jsp.tagext.BodyContent) " + tagHackVar + ");");
        
        if (analyzedTag.getDoInit())
          out.println(name + ".doInitBody();");
        
        if (analyzedTag.getStartReturnsBuffered() &&
	    analyzedTag.getStartReturnsInclude()) {
          out.popDepth();
          out.println("}");

	  // jsp/18kf - req by JSP TCK
	  /*
	  if (_tag.getBodyContent()) {
	    out.println("else");
	    out.println("  " + name + ".setBodyContent((javax.servlet.jsp.tagext.BodyContent) null);");
	  }
	  */
        }
      }
      else if (isBodyTag && _tag.getBodyContent())
        out.println(name + ".setBodyContent((javax.servlet.jsp.tagext.BodyContent) null);");

      if (analyzedTag.getDoAfter() && analyzedTag.getAfterReturnsAgain()) {
	out.println("do {");
	out.pushDepth();
      }

      out.setLocation(getFilename(), getStartLine());

      if (_children != null)
	printVarDeclaration(out, VariableInfo.NESTED);
      
      out.setLocation(getFilename(), getStartLine());
      
      generateChildren(out);

      out.setLocation(getFilename(), getEndLine());

      if (analyzedTag.getDoAfter() && analyzedTag.getAfterReturnsAgain()) {
	out.popDepth();
	out.println("} while (" + name + ".doAfterBody() == javax.servlet.jsp.tagext.IterationTag.EVAL_BODY_AGAIN);");
      }
      else if (analyzedTag.getDoAfter()) {
	out.println(name + ".doAfterBody();");
      }

      if (usesTagBody) {
        if (analyzedTag.getStartReturnsBuffered() &&
	    analyzedTag.getStartReturnsInclude()) {
          out.println("if (_jspEval" + thisId + " == javax.servlet.jsp.tagext.BodyTag.EVAL_BODY_BUFFERED)");

	  if (hasEndTag)
	    out.println("  out = pageContext.popBody();");
	  else
	    out.println("  out = pageContext.popAndReleaseBody();");
        }
        else if (analyzedTag.getStartReturnsBuffered()) {
	  if (hasEndTag)
	    out.println("out = pageContext.popBody();");
	  else
	    out.println("out = pageContext.popAndReleaseBody();");
	}
      }

      if (startCount > 1 && analyzedTag.getStartReturnsSkip()) {
        out.popDepth();
        out.println("}");
      }
      else if (isEmpty) {
      }
      else if ((hasVarDeclaration(VariableInfo.NESTED) ||
		childHasScriptlet()) &&
	       ! (analyzedTag.getDoCatch() ||
		  analyzedTag.getDoFinally() ||
		  analyzedTag.getDoAfter() &&
		  analyzedTag.getAfterReturnsAgain())) {
        out.popDepth();
        out.println("}");
      }
    }

    out.setLocation(getFilename(), getEndLine());
    
    int endCount = ((analyzedTag.getEndReturnsSkip() ? 1 : 0) +
		    (analyzedTag.getEndReturnsEval() ? 1 : 0));
    
    String endVar = "_jsp_end_" + _gen.uniqueId();

    if (! hasEndTag) {
    }
    else if (endCount > 1)
      out.println("int " + endVar + " = " + name + ".doEndTag();");
    else
      out.println(name + ".doEndTag();");

    if (! hasEndTag || ! usesTagBody) {
    }
    else if (hasStartTag &&
	     (analyzedTag.getStartReturnsSkip() ||
	      analyzedTag.getStartReturnsInclude())) {
      out.println("if (" + tagHackVar + " != null) {");
      out.println("  pageContext.releaseBody(" + tagHackVar + ");");
      out.println("  " + tagHackVar + " = null;");
      out.println("}");
    }
    else {
      out.println("pageContext.releaseBody(" + tagHackVar + ");");
    }

    if (analyzedTag.getEndReturnsSkip()) {
      if (hasEndTag && endCount > 1)
	out.println("if (" + endVar + " == javax.servlet.jsp.tagext.Tag.SKIP_PAGE)");
      else
	out.println("if (true)");

      if (_gen.isTag() || isInFragment())
	out.println("  throw new SkipPageException();");
      else
	out.println("  return;");
    }
    
    if (analyzedTag.getDoCatch()) {
      String t = "_jsp_exn_" + _gen.uniqueId();
      
      out.popDepth();
      out.println("} catch (Throwable " + t + ") {");
      out.println("  pageContext.setWriter(" + oldTag + ");");
      out.println("  out = " + oldTag + ";");
      out.println("  " + name + ".doCatch(" + t + ");");
      out.pushDepth();
    }
    
    if (analyzedTag.getDoFinally()) {
      out.popDepth();
      out.println("} finally {");
      out.println("  " + name + ".doFinally();");
      out.pushDepth();
    }

    if (analyzedTag.getDoCatch() || analyzedTag.getDoFinally()) {
      out.popDepth();
      out.println("}");
    }
    
    printVarDeclaration(out, VariableInfo.AT_END);
    
    out.setLocation(getFilename(), getEndLine());
  }

  /**
   * Returns true if the tag implements the named method, false if it
   * relies on the default.
   */
  private boolean tagImplementsMethod(Class cl, String name)
    throws NoSuchMethodException
  {
    Method method;

    try {
      method = cl.getMethod(name, new Class[0]);
    } catch (Exception e) {
      return false;
    }

    if (method == null)
      return false;

    Class declaringClass = method.getDeclaringClass();

    return (! declaringClass.equals(TagSupport.class) &&
            ! declaringClass.equals(BodyTagSupport.class));
  }

  /**
   * Generates the initialization code for the tag.
   *
   * @param out the output stream
   */
  private void generateTagInit(JspJavaWriter out)
    throws Exception
  {
    TagInstance parent = _tag.getParent();
    String id = _tag.getId();

    String var = _tag.getId();
    String className = _tag.getTagClass().getName();
      
    out.print(var + " = new ");
    out.printClass(_tag.getTagClass());
    out.println("();");

    JspNode parentTagNode = getParent().getParentTagNode();

    out.println(var + ".setPageContext(pageContext);");
    if (parentTagNode == null) {
      out.println(var + ".setParent((javax.servlet.jsp.tagext.Tag) null);");
    }
    else if (parentTagNode.isSimpleTag()) {
      String parentName = parentTagNode.getCustomTagName();
      
      out.println("if (" + parentName + "_adapter == null)");
      out.println("  " + parentName + "_adapter = new javax.servlet.jsp.tagext.TagAdapter(" + parentName + ");");
      out.println(var + ".setParent(" + parentName + "_adapter);");
    }
    else {
      String parentName = parentTagNode.getCustomTagName();
      
      out.println(var + ".setParent((javax.servlet.jsp.tagext.Tag) " + parentName + ");");
    }

    ArrayList<QName> names = _tag.getAttributeNames();
    for (int i = 0; i < names.size(); i++) {
      QName name = names.get(i);

      String value = _tag.getAttribute(name);
      if (value == null)
        continue;

      generateSetAttribute(out, var, name, value, false, false);
    }
  }

  /**
   * Returns true if the node or one of its children is a scriptlet
   */
  private boolean childHasScriptlet()
  {
    ArrayList<JspNode> children = getChildren();

    if (children == null)
      return false;

    for (int i = 0; i < children.size(); i++) {
      JspNode child = children.get(i);

      if (child instanceof JspScriptlet || child instanceof JspExpression)
	return true;
    }

    return false;
  }
}
