/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.jexl3.internal;

import org.apache.commons.jexl3.JxltEngine;
import org.apache.commons.jexl3.internal.TemplateEngine.CompositeExpression;
import org.apache.commons.jexl3.internal.TemplateEngine.ConstantExpression;
import org.apache.commons.jexl3.internal.TemplateEngine.DeferredExpression;
import org.apache.commons.jexl3.internal.TemplateEngine.ImmediateExpression;
import org.apache.commons.jexl3.internal.TemplateEngine.NestedExpression;
import org.apache.commons.jexl3.internal.TemplateEngine.TemplateExpression;

import org.apache.commons.jexl3.parser.ASTBlock;
import org.apache.commons.jexl3.parser.ASTFunctionNode;
import org.apache.commons.jexl3.parser.ASTIdentifier;
import org.apache.commons.jexl3.parser.ASTJexlScript;
import org.apache.commons.jexl3.parser.ASTNumberLiteral;
import org.apache.commons.jexl3.parser.JexlNode;

/**
 * A visitor for templates.
 * <p>A friend (ala C++) of template engine.
 */
public class TemplateDebugger extends Debugger {
    /** The outer script. */
    private ASTJexlScript script;
    /** The expressions called by the script through jexl:print. */
    private TemplateExpression[] exprs;

    /**
     * Default ctor.
     */
    public TemplateDebugger() {
    }

    /**
     * Position the debugger on the root of a template expression.
     * @param je the expression
     * @return true if the expression was a {@link TemplateExpression} instance, false otherwise
     */
    public boolean debug(JxltEngine.Expression je) {
        if (je instanceof TemplateExpression) {
            TemplateEngine.TemplateExpression te = (TemplateEngine.TemplateExpression) je;
            return visit(te, this) != null;
        } else {
            return false;
        }
    }

    /**
     * Position the debugger on the root of a template script.
     * @param jt the template
     * @return true if the template was a {@link TemplateScript} instance, false otherwise
     */
    public boolean debug(JxltEngine.Template jt) {
        if (jt instanceof TemplateScript) {
            TemplateScript ts = (TemplateScript) jt;
            // ensure expr is not null for templates
            this.exprs = ts.getExpressions() == null? new TemplateExpression[0] : ts.getExpressions();
            this.script = ts.getScript();
            start = 0;
            end = 0;
            indentLevel = 0;
            builder.setLength(0);
            cause = script;
            int num = script.jjtGetNumChildren();
            for (int i = 0; i < num; ++i) {
                JexlNode child = script.jjtGetChild(i);
                acceptStatement(child, null);
            }
            if (builder.length() > 0 && builder.charAt(builder.length() - 1) != '\n') {
                builder.append('\n');
            }
            end = builder.length();
            return end > 0;
        } else {
            return false;
        }
    }


    @Override
    protected Object visit(ASTBlock node, Object data) {
        // if not really a template, use super impl
        if (exprs == null) {
            return super.visit(node, data);
        }
        // open the block
        builder.append('{');
        if (indent > 0) {
            indentLevel += 1;
            builder.append('\n');
        } else {
            builder.append(' ');
        }
        int num = node.jjtGetNumChildren();
        for (int i = 0; i < num; ++i) {
            JexlNode child = node.jjtGetChild(i);
            acceptStatement(child, data);
        }
        // before we close this block node, $$ might be needed
        newJexlLine();
        if (indent > 0) {
            indentLevel -= 1;
            for (int i = 0; i < indentLevel; ++i) {
                for(int s = 0; s < indent; ++s) {
                    builder.append(' ');
                }
            }
        }
        builder.append('}');
        // closed the block
        return data;
    }

    @Override
    protected Object acceptStatement(JexlNode child, Object data) {
        // if not really a template, use super impl
        if (exprs != null) {
            int printe = getPrintStatement(child);
            if (printe >= 0) {
                // statement is an expr
                TemplateExpression te = exprs[printe];
                return visit(te, data);
            }
            // if statement is not a jexl:print(...), need to prepend '$$'
            newJexlLine();
        }
        return super.acceptStatement(child, data);
    }

    /**
     * In a template, any statement that is not 'jexl:print(n)' must be prefixed by "$$".
     * @param child the node to check
     * @return the expression number or -1 if the node is not a jexl:print
     */
    private int getPrintStatement(JexlNode child) {
        if (child instanceof ASTFunctionNode) {
            ASTFunctionNode node = (ASTFunctionNode) child;
            int num = node.jjtGetNumChildren();
            if (num == 3) {
                ASTIdentifier ns = (ASTIdentifier) node.jjtGetChild(0);
                ASTIdentifier fn = (ASTIdentifier) node.jjtGetChild(1);
                JexlNode args = node.jjtGetChild(2);
                if ("jexl".equals(ns.getName())
                    && "print".equals(fn.getName())
                    && args.jjtGetNumChildren() == 1
                    && args.jjtGetChild(0) instanceof ASTNumberLiteral) {
                    ASTNumberLiteral exprn = (ASTNumberLiteral) args.jjtGetChild(0);
                    int n = exprn.getLiteral().intValue();
                    if (exprs != null && n >= 0 && n < exprs.length) {
                        return n;
                    }
                }
            }
        }
        return -1;
    }

    /**
     * Insert $$ and \n when needed.
     */
    private void newJexlLine() {
        int length = builder.length();
        if (length == 0) {
            builder.append("$$ ");
        } else {
            for (int i = length - 1; i >= 0; --i) {
                char c = builder.charAt(i);
                if (c == '\n') {
                    builder.append("$$ ");
                    break;
                }
                if (c == '}') {
                    builder.append("\n$$ ");
                    break;
                }
                if (c != ' ') {
                    break;
                }
            }
        }
    }

    /**
     * Visit a template expression.
     * @param expr the constant expression
     * @param data the visitor argument
     * @return the visitor argument
     */
    private Object visit(TemplateExpression expr, Object data) {
        Object r;
        switch (expr.getType()) {
            case CONSTANT:
                r = visit((ConstantExpression) expr, data);
                break;
            case IMMEDIATE:
                r = visit((ImmediateExpression) expr, data);
                break;
            case DEFERRED:
                r = visit((DeferredExpression) expr, data);
                break;
            case NESTED:
                r = visit((NestedExpression) expr, data);
                break;
            case COMPOSITE:
                r = visit((CompositeExpression) expr, data);
                break;
            default:
                r = null;
        }
        return r;
    }

    /**
     * Visit a constant expression.
     * @param expr the constant expression
     * @param data the visitor argument
     * @return the visitor argument
     */
    private Object visit(ConstantExpression expr, Object data) {
        expr.asString(builder);
        return data;
    }

    /**
     * Visit an immediate expression.
     * @param expr the immediate expression
     * @param data the visitor argument
     * @return the visitor argument
     */
    private Object visit(ImmediateExpression expr, Object data) {
        builder.append(expr.isImmediate() ? '$' : '#');
        builder.append('{');
        super.accept(expr.node, data);
        builder.append('}');
        return data;
    }

    /**
     * Visit a deferred expression.
     * @param expr the deferred expression
     * @param data the visitor argument
     * @return the visitor argument
     */
    private Object visit(DeferredExpression expr, Object data) {
        builder.append(expr.isImmediate() ? '$' : '#');
        builder.append('{');
        super.accept(expr.node, data);
        builder.append('}');
        return data;
    }

    /**
     * Visit a nested expression.
     * @param expr the nested expression
     * @param data the visitor argument
     * @return the visitor argument
     */
    private Object visit(NestedExpression expr, Object data) {
        super.accept(expr.node, data);
        return data;
    }
    /**
     * Visit a composite expression.
     * @param expr the composite expression
     * @param data the visitor argument
     * @return the visitor argument
     */
    private Object visit(CompositeExpression expr, Object data) {
        for (TemplateExpression ce : expr.exprs) {
            visit(ce, data);
        }
        return data;
    }

}
