/*
 * 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
 *
 *      https://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() {
        // nothing to initialize
    }

    @Override
    protected Object acceptStatement(final JexlNode child, final Object data) {
        // if not really a template, must use super impl
        if (exprs == null) {
            return super.acceptStatement(child, data);
        }
        final TemplateExpression te = getPrintStatement(child);
        if (te != null) {
            // if statement is a jexl:print(...), may need to prepend '\n'
            newJxltLine();
            return visit(te, data);
        }
        // if statement is not a jexl:print(...), need to prepend '$$'
        newJexlLine();
        return super.acceptStatement(child, data);
    }

    /**
     * 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(final JxltEngine.Expression je) {
        if (je instanceof TemplateExpression) {
            final TemplateEngine.TemplateExpression te = (TemplateEngine.TemplateExpression) je;
            return visit(te, this) != null;
        }
        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(final JxltEngine.Template jt) {
        if (!(jt instanceof TemplateScript)) {
            return false;
        }
        final 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;
        final int num = script.jjtGetNumChildren();
        for (int i = 0; i < num; ++i) {
            final JexlNode child = script.jjtGetChild(i);
            acceptStatement(child, null);
        }
        // the last line
        if (builder.length() > 0 && builder.charAt(builder.length() - 1) != '\n') {
            builder.append('\n');
        }
        end = builder.length();
        return end > 0;
    }

    /**
     * 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 TemplateExpression getPrintStatement(final JexlNode child) {
        if (exprs != null && child instanceof ASTFunctionNode) {
            final ASTFunctionNode node = (ASTFunctionNode) child;
            final ASTIdentifier ns = (ASTIdentifier) node.jjtGetChild(0);
            final JexlNode args = node.jjtGetChild(1);
            if ("jexl".equals(ns.getNamespace())
                && "print".equals(ns.getName())
                && args.jjtGetNumChildren() == 1
                && args.jjtGetChild(0) instanceof ASTNumberLiteral) {
                final ASTNumberLiteral exprn = (ASTNumberLiteral) args.jjtGetChild(0);
                final int n = exprn.getLiteral().intValue();
                if (n >= 0 && n < exprs.length) {
                    return exprs[n];
                }
            }
        }
        return null;
    }

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

    /**
     * Insert \n when needed.
     */
    private void newJxltLine() {
        final int length = builder.length();
        for (int i = length - 1; i >= 0; --i) {
            final char c = builder.charAt(i);
            switch (c) {
                case '\n':
                case ';':
                    return;
                case '}':
                    builder.append('\n');
                    return;
                default: // continue
            }
        }
    }

    @Override
    public void reset() {
        super.reset();
        // so we can use it more than one time
        exprs = null;
        script = null;
    }

    @Override
    protected Object visit(final ASTBlock node, final Object data) {
        // if not really a template, must 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(' ');
        }
        final int num = node.jjtGetNumChildren();
        for (int i = 0; i < num; ++i) {
            final 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;
    }

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

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

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

    /**
     * Visit an immediate expression.
     *
     * @param expr the immediate expression
     * @param data the visitor argument
     * @return the visitor argument
     */
    private Object visit(final ImmediateExpression expr, final 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(final NestedExpression expr, final Object data) {
        super.accept(expr.node, data);
        return data;
    }

    /**
     * Visit a template expression.
     *
     * @param expr the constant expression
     * @param data the visitor argument
     * @return the visitor argument
     */
    private Object visit(final TemplateExpression expr, final 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;
    }

}
