/*
 * JSPTem - use jsp's for all your templating!
 * Copyright (c) 2006, Paul Tuckey
 * All rights reserved.
 *
 * JSPTem is based on GNUJSP (Copyright (C) 1999, Yaroslav Faybishenko <yaroslav@cs.berkeley.edu>)
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * This program 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.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */
package org.tuckey.jsptem;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.StringTokenizer;

class JspEmitterStringBuffer {
    public static final String HTTP_JSP_PAGE_IMPL = "org.tuckey.jsptem.HttpJspPageImpl";
    public static final String HTTP_JSP_PAGE_UTIL = "org.tuckey.jsptem.HttpJspPageUtil";
    public static final String GNU_JSP_PAGE = JspTemPage.class.getName();

    public static void emit(BufferedWriter w, JspNode node) throws IOException {
        JspEmitterStringBuffer em = new JspEmitterStringBuffer();
        JspEmitterStringBuffer.emit(em, w, node);
    }

    private static void emit(JspEmitterStringBuffer em, BufferedWriter w, JspNode node) throws IOException {
        if (node == null) return;
        JspNode childNode;
        switch (node.getType()) {
            case JspNode.JSP_BODY:
                em.emitBeginPage(w, node);

                childNode = node.getFirstChild();
                while (childNode != null) {
                    JspEmitterStringBuffer.emit(em, w, childNode);
                    childNode = childNode.getNextSibling();
                }

                em.emitEndPage(w, node);
                break;
            case JspNode.JSP_INCLUDE_DIRECTIVE:
                em.emitIncludeDirective(w,
                        node,
                        node.getAttribute("file"));
                break;
            case JspNode.TEMPLATE_TEXT:
                em.emitTemplateText(w, node, node.getAttribute("text"));
                break;
            case JspNode.JSP_EXPRESSION:
                em.emitBeginPrintExprCall(w, node);
                em.emitPrintExprCall(w, node, node.getFirstChild().getAttribute("text"));
                em.emitEndPrintExprCall(w);
                break;
            case JspNode.JSP_SCRIPTLET:
                em.emitBeginScriptlet(w, node);
                em.emitScriptlet(w, node, node.getFirstChild().getAttribute("text"));
                em.emitEndScriptlet(w);
                break;
            case JspNode.JSP_INCLUDE:
                em.emitInclude(w, node, node.getAttribute("page"), node.getAttribute("flush"));
                break;
            case JspNode.JSP_FORWARD:
                em.emitForward(w, node, node.getAttribute("page"));
                break;
            case JspNode.JSP_USEBEAN:
                em.emitBeginUseBean(w, node,
                        node.getAttribute("id"),
                        node.getAttribute("scope"),
                        node.getAttribute("type"),
                        node.getAttribute("class"),
                        node.getAttribute("beanName"));

                childNode = node.getFirstChild();
                while (childNode != null) {
                    JspEmitterStringBuffer.emit(em, w, childNode);
                    childNode = childNode.getNextSibling();
                }

                em.emitEndUseBean(w);
                break;
            case JspNode.JSP_GETPROPERTY:
                em.emitGetProperty(w, node,
                        node.getAttribute("name"),
                        node.getAttribute("property"));
                break;
            case JspNode.JSP_SETPROPERTY:
                em.emitSetProperty(w,
                        node,
                        node.getAttribute("name"),
                        node.getAttribute("property"),
                        node.getAttribute("param"),
                        node.getAttribute("value"));
                break;
        }
    }

    public static void emitPos(BufferedWriter w, JspNode node)
            throws IOException {
        Pos pos = node.getPos();

        if (pos != null) {
            w.write(pos.toJavaCode());
            w.newLine();
        }
    }

    public void emitBeginPage(BufferedWriter w, JspNode node) throws IOException {
        Pos pos = node.getPos();

        // Default imports are specified by JSP 1.1PR2 2.7.1
        String import_ = node.getAttribute("import", "") +
                ",java.lang.Exception,java.lang.StringBuffer";

        String declaration = node.getAttribute("declaration", "");
        String package_ = node.getAttribute("package");
        String class_ = node.getAttribute("class");
        String info = node.getAttribute("info");

        if (class_ == null)
            throw new ParseException("className not set");

        String isThreadSafeStr = node.getAttribute("isThreadSafe", "true");
        if (!("true".equals(isThreadSafeStr) || "false".equals(isThreadSafeStr)))
            throw new ParseException(pos, "ERR_sp10_2_7_1_invalid_value_isthreadsafe");

        Boolean isThreadSafe = Boolean.valueOf(isThreadSafeStr);
        String extends_ = node.getAttribute("extends", JspEmitterStringBuffer.HTTP_JSP_PAGE_IMPL);

        String isErrorPageStr = node.getAttribute("isErrorPage", "false");
        if (!("true".equals(isErrorPageStr) || "false".equals(isErrorPageStr)))
            throw new ParseException(pos, "ERR_sp10_2_7_1_invalid_value_iserrorpage");

        w.write("/*");
        w.newLine();
        w.write("   Generated by JspTem " + JspEmitterStringBuffer.class.getSimpleName() + " on " + new Date());
        w.newLine();
        w.write("   Please do NOT modify.");
        w.newLine();
        w.write(" */");
        w.newLine();
        w.newLine();

        if (package_ != null) {
            w.write("package " + package_ + ";");
            w.newLine();
            w.newLine();
        }

        StringTokenizer ie = new StringTokenizer(import_, ",");
        while (ie.hasMoreElements()) {
            String imp = ie.nextToken();
            if (!("".equals(imp))) {
                w.write("import " + imp + ";");
                w.newLine();
            }
        }

        w.newLine();
        w.write("public class " + class_);

        if (! JspEmitterStringBuffer.HTTP_JSP_PAGE_IMPL.equals(extends_)) {
            w.write(" extends " + extends_);
        }
        if (!isThreadSafe.booleanValue()) {
            w.write(" implements SingleThreadModel");
        }

        w.write("{");
        w.newLine();
        w.write(declaration);
        w.newLine();

        w.write("   private static final String[] _gnujspDeps = new String[] { ");
        URL[] deps = (URL[]) node.getPageAttributeObject("gnujspDeps");
        for (int i = 0; i < deps.length; i++) {
            if (i > 0) w.write(", ");
            enquote(w, deps[i].toString());
        }
        w.write(" };");
        w.newLine();
        w.write("   public final String[] _gnujspGetDeps() {");
        w.newLine();
        w.write("      return _gnujspDeps;");
        w.newLine();
        w.write("   }");
        w.newLine();

        w.write("   public final long _gnujspGetCompilerVersion() {");
        w.newLine();
        w.write("      return " + 1 + "L;"); // todo: node.getAttribute("gnujspCompilerVersion")
        w.newLine();
        w.write("   }");
        w.newLine();

        if (info != null) {
            w.write("   public String getServletInfo() {");
            w.newLine();
            w.write("     return ");
            enquote(w, info);
            w.write(";");
            w.newLine();
            w.write("   }");
            w.newLine();
            w.newLine();
        }

        w.write("   public String getHTML() throws Exception {");
        w.newLine();
        w.newLine();
        w.write("       StringBuffer sb = new StringBuffer();");
        w.newLine();
        w.newLine();
        w.flush();
    }

    public void emitIncludeDirective(BufferedWriter w, JspNode n,
                                     String file) throws IOException {
        // do nothing for direct java code generation
    }

    public void emitBeginScriptlet(BufferedWriter w, JspNode n) throws IOException {
        JspEmitterStringBuffer.emitPos(w, n);
        // do nothing for direct java code generation
    }

    public void emitScriptlet(BufferedWriter w, JspNode n, String code) throws IOException {
        w.write(code);
    }

    public void emitEndScriptlet(BufferedWriter w) throws IOException {
        w.newLine(); // Make sure next output starts on new line
    }

    public void emitBeginPrintExprCall(BufferedWriter w, JspNode n) throws IOException {
        JspEmitterStringBuffer.emitPos(w, n);
        w.write("sb.append(");
    }

    public void emitPrintExprCall(BufferedWriter w, JspNode n, String expr) throws IOException {
        w.write(expr);
    }

    public void emitEndPrintExprCall(BufferedWriter w) throws IOException {
        w.write(");");
        w.newLine();
    }

    public void emitTemplateText(BufferedWriter w, JspNode n, String t) throws IOException {
        w.newLine();
        // make sure we start pos on new line
        JspEmitterStringBuffer.emitPos(w, n);
        w.write("sb.append(");
        enquote(w, t);
        w.write(");");
        w.newLine();
    }

    public void emitForward(BufferedWriter w, JspNode node, String page) throws IOException {
        Pos pos = node.getPos();

        if (page == null) {
            throw new ParseException(pos, "ERR_sp10_2_13_5_missing_attr_page");
        }
        if (pos != null) {
            w.write(pos.toJavaCode());
            w.newLine();
        }
        // 2.13.5 "A jsp:forward effectively terminates the execution of
        // the current page."
        w.write("if(true) {");
        w.newLine();
        // 2.12.1 "The following attributes accept request-time attribute
        // expressions... The page attribute of jsp:forward"
        w.write("   pageContext.forward (" + inlineAttribute(pos, page));
        writeQueryString(w, node);
        w.write(");");
        w.newLine();
        w.write("    return;");
        w.newLine();
        w.write("}");
        w.newLine();
    }

    public void emitInclude(BufferedWriter w, JspNode node, String page, String flush) throws IOException {
        Pos pos = node.getPos();

        if (page == null) {
            throw new ParseException(pos, "missing_attr_page");
        }
        if (flush == null) {
            throw new ParseException(pos, "missing_attr_flush");
        }
        Boolean flushVal = Boolean.valueOf(flush);
        if (!flushVal.booleanValue()) {
            throw new ParseException(pos, "illegal_value_flush_not_true");
        }
        if (pos != null) {
            w.write(pos.toJavaCode());
            w.newLine();
        }

        // 2.12.1 "The following attributes accept request-time attribute
        // expressions... The page attribute of jsp:include"

        w.write("pageContext.include (" + inlineAttribute(pos, page));
        writeQueryString(w, node);
        w.write(");");
        w.newLine();
    }

    private void writeQueryString(BufferedWriter w, JspNode node) throws IOException {
        // If any parameters were set, generate a query string.
        // values are request-time expressions.
        Hashtable params = node.getParameters();
        if (params != null) {
            w.write(" + \"?");
            Pos pos = node.getPos();
            Enumeration e = params.keys();
            String name;
            String[] values;
            boolean first = true;

            while (e.hasMoreElements()) {
                name = (String) e.nextElement();
                values = (String[]) params.get(name);
                for (int i = 0; i < values.length; i++) {
                    String value = values[i];
                    if (!first) w.write(" + \"&");
                    else first = false;
                    w.write(URLEncoder.encode(name, "UTF8"));   // todo: should we be using utf8 here?
                    w.write("=\" + java.net.URLEncoder.encode(");
                    w.write(inlineAttribute(pos, value));
                    w.write(", \"UTF8\")");   // todo: should we be using utf8 here?
                }
            } // for each key
        } // if non-null
    }

    public void emitBeginUseBean(BufferedWriter w, JspNode node, String id, String scope,
                                 String type, String clas, String beanName)
            throws IOException {
        Pos pos = node.getPos();

        if (type == null && clas == null)
            throw new ParseException(pos, "missing_attr_type_or_class");
        if (id == null)
            throw new ParseException(pos, "missing_attr_id");
        if (scope == null) scope = "page";
        if (("session".equals(scope)) && "false".equals(node.getPageAttribute("session")))
            throw new ParseException(pos, "illegal_session_scope");

        if (type == null) type = clas;
        if ((clas != null) && (beanName != null))
            throw new ParseException(pos, "illegal_attr_class_and_beanname" + " " + id);
        if (pos != null) {
            w.write(pos.toJavaCode("+"));
            w.newLine();
        }

        w.write("    " + type + " " + id + " = (" + type + ")");
        w.newLine();
        w.write("       pageContext.getAttribute (\"" +
                id + "\", PageContext." +
                scope.toUpperCase() + "_SCOPE);");
        w.newLine();

        if (clas != null) {
            w.write("    if (" + id + " == null) {");
            w.newLine();
            w.write("        " + id + " = new " + clas + " ();");
            w.newLine();

        } else if (beanName != null) {
            w.write("    if (" + id + " == null) {");
            w.newLine();
            // 2.12.1 "The following attributes accept request-time attribute
            // expressions... The beanName attribute of [jsp:useBean]"
            // Make better error message by catching ClassNotFoundException
            w.write("      try {");
            w.newLine();
            w.write("        " + id + " = (" + type + ") java.beans.Beans.instantiate (" +
                    "this.getClass ().getClassLoader (), " +
                    inlineAttribute(pos, beanName) + ");");
            w.newLine();
            w.write("      } catch(ClassNotFoundException _gnujspex) {");
            w.write("        throw new InstantiationException(\""
                    + "could_not_instantiate_bean_class_not_found)"
                    + " '\"+"
                    + inlineAttribute(pos, beanName) + "+\"'\");");
            w.newLine();
            w.write(" }");
            w.newLine();
        }

        // test if bean could be instantiated, runtime exception !
        w.write("        if (" + id + " == null)");
        w.newLine();
        w.write("            throw new InstantiationException(\""
                + "could_not_instantiate_bean)"
                + ": " + id + "\");");

        if (clas != null || beanName != null) {
            w.write("       pageContext.setAttribute (\"" +
                    id + "\", " + id + ", PageContext." +
                    scope.toUpperCase() + "_SCOPE);");
            w.newLine();
        }

        // type only
        if (clas == null && beanName == null) {
            // 2.13.1 If the object is found ...
            // If the jsp:useBean element had a non-empty body it is ignored.
            // This completes the processing of the useBean action
            w.write("    if (false) {");
            w.newLine();
        }
    }

    public void emitEndUseBean(BufferedWriter w) throws IOException {
        w.write("    }");
        w.newLine();
    }

    public void emitGetProperty(BufferedWriter w, JspNode node, String name, String property)
            throws IOException {
        Pos pos = node.getPos();

        if (name == null) throw new ParseException(pos, "ERR_sp10_2_13_3_missing_attr_name");
        if (property == null) throw new ParseException(pos, "ERR_sp10_2_13_3_missing_attr_property");

        if (pos != null) {
            w.write(pos.toJavaCode());
            w.newLine();
        }
        w.write("    sb.append("
                + JspEmitterStringBuffer.HTTP_JSP_PAGE_UTIL + ".getProperty ("
                + name + ", \"" + property + "\", \"" + name + "\"));");
        w.newLine();
    }

    public void emitSetProperty(BufferedWriter w, JspNode node,
                                String name, String property,
                                String param, String value)
            throws IOException {
        Pos pos = node.getPos();

        if (property == null) {
            throw new ParseException(pos, "ERR_sp10_2_13_2_missing_attr_property");
        }

        if (name == null) throw new ParseException(pos, "ERR_sp10_2_13_2_missing_attr_name");
        if (param != null && value != null)
            throw new ParseException(pos, "ERR_sp10_2_13_2_illegal_attr_param_and_value");

        if (pos != null) {
            w.write(pos.toJavaCode());
            w.newLine();
        }

        // setProperty name="foo" property="bar" looks for param "bar".
        if (param == null && value == null) param = property;

        if (property.equals("*")) {
            w.write("    " + JspEmitterStringBuffer.HTTP_JSP_PAGE_UTIL
                    + ".setProperties (" + name + ", request);");
            w.newLine();

        } else if (param != null) {
            w.write("    if (true) {");
            w.newLine();// Needed for scoping s.
            // 2.13.2 If parameter has a value of "" the
            // corresponding property is not modified.
            // we detect this by the following logic on request.getParameterValues():
            // null OR length is 0 OR length is 1 AND (element 0 is null
            // or the empty string).

            // Always pass param as a String[]; Util class decides whether
            // to use it as a String or an array.
            w.write("      String[] s = request.getParameterValues(\""
                    + param + "\");");
            w.newLine();
            w.write("      if ((s != null && s.length > 0) && ");
            w.write("(!(s.length == 1 && (s[0] == null || s[0].equals(\"\"))))) ");
            w.newLine();

            w.write("           " + JspEmitterStringBuffer.HTTP_JSP_PAGE_UTIL
                    + ".setProperty (" + name + ",");
            w.newLine();
            w.write("                       \"" + property + "\",");
            w.newLine();
            w.write("                       s);");
            w.newLine();
            w.write("    }");
            w.newLine();
        } else {
            // 2.12.1 "The following attributes accept request-time attribute
            // expressions... The value attribute of jsp:setProperty"
            w.write("    " + JspEmitterStringBuffer.HTTP_JSP_PAGE_UTIL + ".setProperty ("
                    + name + ", " + "\"" + property +
                    "\", " + JspEmitterStringBuffer.HTTP_JSP_PAGE_UTIL + ".objectify("
                    + inlineAttribute(pos, value) + "));");
            w.newLine();
        }
    }

    public void emitEndPage(BufferedWriter w, JspNode n) throws IOException {
        w.write("       // } catch (Throwable e) {");
        w.newLine();
        //  w.write ("             sb.append();\n");
        // I know, this does not follow the example in the 1.0 spec
        // but we need to handle and report exceptions after
        // flush has been called (e.g. by pageContext.include())
        // if someone comes with a better solution, tell me. (alph)
        w.write("             // out.clearBuffer ();");
        w.newLine();
        w.write("            // if (e instanceof Exception) pageContext.handlePageException ((Exception) e);");
        w.newLine();
        w.write("            // else pageContext.handlePageException (new Exception(e.getMessage()));");
        w.newLine();
        w.write("     //   } finally {");
        w.newLine();
        // 29.8.2000 alph
        // using close() here result in problems in some jsp engines when including
        // jsp pages. So we just flush now.
        //  w.write ("             out.close ();"); w.newLine ();
        w.write("           //  out.flush ();");
        w.newLine();
        w.write("            // factory.releasePageContext (pageContext);");
        w.newLine();
        w.write("     //   }");
        w.newLine();
        w.write("     return sb.toString();");
        w.newLine();
        w.newLine();
        w.write("   }");
        w.newLine();
        w.newLine();
        w.write("}");
        w.newLine();

        w.flush();
    }

    /**
     * Converts a string into a format that can be used in a
     * print() function, surrounding it with double quotes and
     * escaping any necessary control characters.
     */
    void enquote(BufferedWriter w, String t) throws IOException {
        w.write('"');
        char c;
        for (int i = 0; i < t.length(); i++) {
            switch (c = t.charAt(i)) {
                case '\t':
                    w.write("\\t");
                    break;
                case '\n':
                    w.write("\\n");
                    break;
                case '\r':
                    w.write("\\r");
                    break;
                case '\\':
                    w.write("\\\\");
                    break;
                case '\"':
                    w.write("\\");
                default:
                    w.write(c);
            }
        }
        w.write('"');
    }

    /**
     * Translates a String argument, encoding any necessary request-time
     * arguments, and prepending and appending a double-quote character.
     * Also, escape any double-quotes, etc., in the string.
     * <p/>
     * Example 1:               "foo" --> "\"foo\""
     * Example 2:       "<%= name %>" --> "\"" + name + "\""
     * <p/>
     * Output is suitable as the parameter to out.print(_____)
     */
    String inlineAttribute(Pos pos, String val) throws IOException {
        int pos1 = val.indexOf("<%=");
        int pos2 = val.indexOf("%>");
        if ((pos1 == 0) && (pos2 == val.length() - 2)) {
            return val.substring(3, val.length() - 2);
        }
        if ((pos1 >= 0) && (pos1 < pos2)) {
            throw new ParseException(pos, "ERR_sp10_2_12_1_invalid_value_expression_and_string)" + ": " + val);
        }

        StringBuffer b = new StringBuffer();
        int prev = 0;
        int i = 0;
        do {
            pos1 = val.indexOf("<\\%", i);
            pos2 = val.indexOf("%\\>", i);
            if (pos1 == pos2) break; // both are -1, we're done.
            i = 1 + (((pos2 == -1) || ((pos1 >= 0) && (pos1 < pos2)))
                    ? pos1 : pos2);
            b.append(val.substring(prev, i));
            prev = ++i;
        } while (true);
        b.append(val.substring(prev));
        StringWriter w = new StringWriter();
        BufferedWriter bw = new BufferedWriter(w);
        enquote(bw, b.toString());
        bw.flush();
        return w.toString();
    }
}
