/*
 * 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 java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.jexl3.JexlCache;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.JexlFeatures;
import org.apache.commons.jexl3.JexlInfo;
import org.apache.commons.jexl3.JexlOptions;
import org.apache.commons.jexl3.JxltEngine;
import org.apache.commons.jexl3.parser.ASTJexlScript;
import org.apache.commons.jexl3.parser.JexlNode;
import org.apache.commons.jexl3.parser.StringParser;
import org.apache.commons.logging.Log;

/**
 * A JxltEngine implementation.
 *
 * @since 3.0
 */
public final class TemplateEngine extends JxltEngine {
    /**
     * Abstract the source fragments, verbatim or immediate typed text blocks.
     */
    static final class Block {
        /** The type of block: verbatim or directive. */
        private final BlockType type;

        /** The block start line info. */
        private final int line;

        /** The actual content. */
        private final String body;

        /**
         * Creates a new block.
         *
         * @param theType  the block type
         * @param theLine  the line number
         * @param theBlock the content
         */
        Block(final BlockType theType, final int theLine, final String theBlock) {
            type = theType;
            line = theLine;
            body = theBlock;
        }

        /**
         * @return body
         */
        String getBody() {
            return body;
        }

        /**
         * @return line
         */
        int getLine() {
            return line;
        }

        /**
         * @return type
         */
        BlockType getType() {
            return type;
        }

        /**
         * Appends this block string representation to a builder.
         *
         * @param strb   the string builder to append to
         * @param prefix the line prefix (immediate or deferred)
         */
        void toString(final StringBuilder strb, final String prefix) {
            if (BlockType.VERBATIM.equals(type)) {
                strb.append(body);
            } else {
                final Iterator<CharSequence> lines = readLines(new StringReader(body));
                while (lines.hasNext()) {
                    strb.append(prefix).append(lines.next());
                }
            }
        }
    }

    /**
     * The enum capturing the difference between verbatim and code source fragments.
     */
    enum BlockType {
        /** Block is to be output "as is" but may be a unified expression. */
        VERBATIM,

        /** Block is a directive, i.e., a fragment of JEXL code. */
        DIRECTIVE
    }

    /** A composite unified expression: "... ${...} ... #{...} ...". */
    final class CompositeExpression extends TemplateExpression {
        /** Bit encoded (deferred count > 0) bit 1, (immediate count > 0) bit 0. */
        private final int meta;

        /** The list of sub-expression resulting from parsing. */
        final TemplateExpression[] exprs;

        /**
         * Creates a composite expression.
         *
         * @param counters counters of expressions per type
         * @param list     the sub-expressions
         * @param src      the source for this expression if any
         */
        CompositeExpression(final int[] counters, final List<TemplateExpression> list, final TemplateExpression src) {
            super(src);
            this.exprs = list.toArray(new TemplateExpression[0]);
            this.meta = (counters[ExpressionType.DEFERRED.getIndex()] > 0 ? 2 : 0)
                    | (counters[ExpressionType.IMMEDIATE.getIndex()] > 0 ? 1 : 0);
        }

        @Override
        public StringBuilder asString(final StringBuilder strb) {
            for (final TemplateExpression e : exprs) {
                e.asString(strb);
            }
            return strb;
        }

        @Override
        protected Object evaluate(final Interpreter interpreter) {
            Object value;
            // common case: evaluate all expressions & concatenate them as a string
            final StringBuilder strb = new StringBuilder();
            for (final TemplateExpression expr : exprs) {
                value = expr.evaluate(interpreter);
                if (value != null) {
                    strb.append(value);
                }
            }
            return strb.toString();
        }

        @Override
        ExpressionType getType() {
            return ExpressionType.COMPOSITE;
        }

        @Override
        public Set<List<String>> getVariables() {
            final Engine.VarCollector collector = jexl.varCollector();
            for (final TemplateExpression expr : exprs) {
                expr.getVariables(collector);
            }
            return collector.collected();
        }

        /**
         * Fills up the list of variables accessed by this unified expression.
         *
         * @param collector the variable collector
         */
        @Override
        protected void getVariables(final Engine.VarCollector collector) {
            for (final TemplateExpression expr : exprs) {
                expr.getVariables(collector);
            }
        }

        @Override
        public boolean isImmediate() {
            // immediate if no deferred
            return (meta & 2) == 0;
        }

        @Override
        protected TemplateExpression prepare(final Interpreter interpreter) {
            // if this composite is not its own source, it is already prepared
            if (source != this) {
                return this;
            }
            // we need to prepare all sub-expressions
            final int size = exprs.length;
            final ExpressionBuilder builder = new ExpressionBuilder(size);
            // tracking whether prepare will return a different expression
            boolean eq = true;
            for (final TemplateExpression expr : exprs) {
                final TemplateExpression prepared = expr.prepare(interpreter);
                // add it if not null
                if (prepared != null) {
                    builder.add(prepared);
                }
                // keep track of TemplateExpression equivalence
                eq &= expr == prepared;
            }
            return eq ? this : builder.build(TemplateEngine.this, this);
        }
    }

    /** A constant unified expression. */
    final class ConstantExpression extends TemplateExpression {
        /** The constant held by this unified expression. */
        private final Object value;

        /**
         * Creates a constant unified expression.
         *
         * @param val    the constant value
         * @param source the source TemplateExpression if any
         */
        ConstantExpression(final Object val, final TemplateExpression source) {
            super(source);
            Objects.requireNonNull(val, "val");
            this.value = val instanceof String
                    ? StringParser.buildTemplate((String) val, false)
                    : val;
        }

        @Override
        public StringBuilder asString(final StringBuilder strb) {
            if (value != null) {
                strb.append(value);
            }
            return strb;
        }

        @Override
        protected Object evaluate(final Interpreter interpreter) {
            return value;
        }

        @Override
        ExpressionType getType() {
            return ExpressionType.CONSTANT;
        }
    }

    /** A deferred unified expression: #{jexl}. */
    final class DeferredExpression extends JexlBasedExpression {
        /**
         * Creates a deferred unified expression.
         *
         * @param expr   the unified expression as a string
         * @param node   the unified expression as an AST
         */
        DeferredExpression(final CharSequence expr, final JexlNode node) {
            super(expr, node, null);
        }

        @Override
        ExpressionType getType() {
            return ExpressionType.DEFERRED;
        }

        @Override
        protected void getVariables(final Engine.VarCollector collector) {
            // noop
        }

        @Override
        public boolean isImmediate() {
            return false;
        }

        @Override
        protected TemplateExpression prepare(final Interpreter interpreter) {
            return new ImmediateExpression(expr, node, source);
        }
    }

    /**
     * A helper class to build expressions.
     * Keeps count of sub-expressions by type.
     */
    static final class ExpressionBuilder {
        /** Per TemplateExpression type counters. */
        private final int[] counts;

        /** The list of expressions. */
        private final List<TemplateExpression> expressions;

        /**
         * Creates a builder.
         *
         * @param size the initial TemplateExpression array size
         */
        ExpressionBuilder(final int size) {
            counts = new int[]{0, 0, 0};
            expressions = new ArrayList<>(size <= 0 ? 3 : size);
        }

        /**
         * Adds an TemplateExpression to the list of expressions, maintain per-type counts.
         *
         * @param expr the TemplateExpression to add
         */
        void add(final TemplateExpression expr) {
            counts[expr.getType().getIndex()] += 1;
            expressions.add(expr);
        }

        /**
         * Builds a TemplateExpression from a source, performs checks.
         *
         * @param el     the unified el instance
         * @param source the source TemplateExpression
         * @return an TemplateExpression
         */
        TemplateExpression build(final TemplateEngine el, final TemplateExpression source) {
            int sum = 0;
            for (final int count : counts) {
                sum += count;
            }
            if (expressions.size() != sum) {
                final StringBuilder error = new StringBuilder("parsing algorithm error: ");
                throw new IllegalStateException(toString(error).toString());
            }
            // if only one sub-expr, no need to create a composite
            if (expressions.size() == 1) {
                return expressions.get(0);
            }
            return el.new CompositeExpression(counts, expressions, source);
        }

        @Override
        public String toString() {
            return toString(new StringBuilder()).toString();
        }

        /**
         * Base for to-string.
         *
         * @param error the builder to fill
         * @return the builder
         */
        StringBuilder toString(final StringBuilder error) {
            error.append("exprs{");
            error.append(expressions.size());
            error.append(", constant:");
            error.append(counts[ExpressionType.CONSTANT.getIndex()]);
            error.append(", immediate:");
            error.append(counts[ExpressionType.IMMEDIATE.getIndex()]);
            error.append(", deferred:");
            error.append(counts[ExpressionType.DEFERRED.getIndex()]);
            error.append("}");
            return error;
        }
    }

    /**
     * Types of expressions.
     * Each instance carries a counter index per (composite sub-) template expression type.
     *
     * @see ExpressionBuilder
     */
    enum ExpressionType {
        /** Constant TemplateExpression, count index 0. */
        CONSTANT(0),

        /** Immediate TemplateExpression, count index 1. */
        IMMEDIATE(1),

        /** Deferred TemplateExpression, count index 2. */
        DEFERRED(2),

        /** Nested (which are deferred) expressions, count
         * index 2. */
        NESTED(2),

        /** Composite expressions are not counted, index -1. */
        COMPOSITE(-1);

        /** The index in arrays of TemplateExpression counters for composite expressions. */
        private final int index;

        /**
         * Creates an ExpressionType.
         *
         * @param idx the index for this type in counters arrays.
         */
        ExpressionType(final int idx) {
            this.index = idx;
        }

        /**
         * @return the index member
         */
        int getIndex() {
            return index;
        }
    }

    /** An immediate unified expression: ${jexl}. */
    final class ImmediateExpression extends JexlBasedExpression {
        /**
         * Creates an immediate unified expression.
         *
         * @param expr   the unified expression as a string
         * @param node   the unified expression as an AST
         * @param source the source unified expression if any
         */
        ImmediateExpression(final CharSequence expr, final JexlNode node, final TemplateExpression source) {
            super(expr, node, source);
        }

        @Override
        ExpressionType getType() {
            return ExpressionType.IMMEDIATE;
        }

        @Override
        protected TemplateExpression prepare(final Interpreter interpreter) {
            // evaluate immediate as constant
            final Object value = evaluate(interpreter);
            return value != null ? new ConstantExpression(value, source) : null;
        }
    }

    /** The base for JEXL based unified expressions. */
    abstract class JexlBasedExpression extends TemplateExpression {
        /** The JEXL string for this unified expression. */
        protected final CharSequence expr;

        /** The JEXL node for this unified expression. */
        protected final JexlNode node;

        /**
         * Creates a JEXL interpretable unified expression.
         *
         * @param theExpr   the unified expression as a string
         * @param theNode   the unified expression as an AST
         * @param theSource the source unified expression if any
         */
        protected JexlBasedExpression(final CharSequence theExpr, final JexlNode theNode, final TemplateExpression theSource) {
            super(theSource);
            this.expr = theExpr;
            this.node = theNode;
        }

        @Override
        public StringBuilder asString(final StringBuilder strb) {
            strb.append(isImmediate() ? immediateChar : deferredChar);
            strb.append("{");
            strb.append(expr);
            strb.append("}");
            return strb;
        }

        @Override
        protected Object evaluate(final Interpreter interpreter) {
            return interpreter.interpret(node);
        }

        @Override
        JexlInfo getInfo() {
            return node.jexlInfo();
        }

        @Override
        public Set<List<String>> getVariables() {
            final Engine.VarCollector collector = jexl.varCollector();
            getVariables(collector);
            return collector.collected();
        }

        @Override
        protected void getVariables(final Engine.VarCollector collector) {
            jexl.getVariables(node instanceof ASTJexlScript? (ASTJexlScript) node : null, node, collector);
        }

        @Override
        protected JexlOptions options(final JexlContext context) {
            return jexl.evalOptions(node instanceof ASTJexlScript? (ASTJexlScript) node : null, context);
        }
    }

    /**
     * An immediate unified expression nested into a deferred unified expression.
     * #{...${jexl}...}
     * Note that the deferred syntax is JEXL's.
     */
    final class NestedExpression extends JexlBasedExpression {
        private final Scope scope;
        /**
         * Creates a nested unified expression.
         *
         * @param expr   the unified expression as a string
         * @param node   the unified expression as an AST
         */
        NestedExpression(final CharSequence expr, final JexlNode node, final Scope sc) {
            super(expr, node, null);
            this.scope = sc;
            if (source != this) {
                throw new IllegalArgumentException("Nested TemplateExpression cannot have a source");
            }
        }

        @Override
        public StringBuilder asString(final StringBuilder strb) {
            strb.append(expr);
            return strb;
        }

        @Override
        protected Object evaluate(final Interpreter interpreter) {
            return prepare(interpreter).evaluate(interpreter);
        }

        @Override
        ExpressionType getType() {
            return ExpressionType.NESTED;
        }

        @Override
        public boolean isImmediate() {
            return false;
        }

        @Override
        protected TemplateExpression prepare(final Interpreter interpreter) {
            final String value = interpreter.interpret(node).toString();
            final JexlNode dnode = jexl.jxltParse(node.jexlInfo(), noscript, value, scope);
            return new ImmediateExpression(value, dnode, this);
        }
    }

    /** The different parsing states. */
    private enum ParseState {

        /** Parsing a constant. */
        CONST,

        /** Parsing after <code>$</code> . */
        IMMEDIATE0,

        /** Parsing after <code>#</code> . */
        DEFERRED0,

        /** Parsing after <code>${</code> . */
        IMMEDIATE1,

        /** Parsing after <code>#{</code> . */
        DEFERRED1,

        /** Parsing after <code>\</code> . */
        ESCAPE
    }

    /**
     * The abstract base class for all unified expressions, immediate '${...}' and deferred '#{...}'.
     */
    abstract class TemplateExpression implements Expression {
        /** The source of this template expression(see {@link TemplateEngine.TemplateExpression#prepare}). */
        protected final TemplateExpression source;

        /**
         * Creates an TemplateExpression.
         *
         * @param src the source TemplateExpression if any
         */
        TemplateExpression(final TemplateExpression src) {
            this.source = src != null ? src : this;
        }

        @Override
        public String asString() {
            final StringBuilder strb = new StringBuilder();
            asString(strb);
            return strb.toString();
        }

        /**
         * Interprets a sub-expression.
         *
         * @param interpreter a JEXL interpreter
         * @return the result of interpretation
         * @throws JexlException (only for nested and composite)
         */
        protected abstract Object evaluate(Interpreter interpreter);

        @Override
        public final Object evaluate(final JexlContext context) {
            return evaluate(context, null, null);
        }

        /**
         * Evaluates this expression.
         *
         * @param frame the frame storing parameters and local variables
         * @param context the context storing global variables
         * @param options flags and properties that can alter the evaluation behavior.
         * @return the expression value
         * @throws JexlException if expression evaluation fails
         */
        protected final Object evaluate(final JexlContext context, final Frame frame, final JexlOptions options) {
            try {
                final TemplateInterpreter.Arguments args = new TemplateInterpreter.Arguments(jexl).context(context)
                        .options(options != null ? options : options(context)).frame(frame);
                final Interpreter interpreter = jexl.createTemplateInterpreter(args);
                return evaluate(interpreter);
            } catch (final JexlException xjexl) {
                final JexlException xuel = createException(xjexl.getInfo(), "evaluate", this, xjexl);
                if (jexl.isSilent()) {
                    if (logger.isWarnEnabled()) {
                        logger.warn(xuel.getMessage(), xuel.getCause());
                    }
                    return null;
                }
                throw xuel;
            }
        }

        /** @return the info */
        JexlInfo getInfo() {
            return null;
        }

        @Override
        public final TemplateExpression getSource() {
            return source;
        }

        /**
         * Gets this TemplateExpression type.
         *
         * @return its type
         */
        abstract ExpressionType getType();

        @Override
        public Set<List<String>> getVariables() {
            return Collections.emptySet();
        }

        /**
         * Fills up the list of variables accessed by this unified expression.
         *
         * @param collector the variable collector
         */
        protected void getVariables(final Engine.VarCollector collector) {
            // nothing to do
        }

        @Override
        public final boolean isDeferred() {
            return !isImmediate();
        }

        @Override
        public boolean isImmediate() {
            return true;
        }

        /**
         * The options to use during evaluation.
         *
         * @param context the context
         * @return the options
         */
        protected JexlOptions options(final JexlContext context) {
            return jexl.evalOptions(null, context);
        }

        /**
         * Prepares a sub-expression for interpretation.
         *
         * @param interpreter a JEXL interpreter
         * @return a prepared unified expression
         * @throws JexlException (only for nested and composite)
         */
        protected TemplateExpression prepare(final Interpreter interpreter) {
            return this;
        }

        @Override
        public final TemplateExpression prepare(final JexlContext context) {
                return prepare(context, null, null);
        }

        /**
         * Prepares this expression.
         *
         * @param frame the frame storing parameters and local variables
         * @param context the context storing global variables
         * @param options flags and properties that can alter the evaluation behavior.
         * @return the expression value
         * @throws JexlException if expression preparation fails
         */
        protected final TemplateExpression prepare(final JexlContext context, final Frame frame, final JexlOptions options) {
            try {
                final TemplateInterpreter.Arguments args = new TemplateInterpreter.Arguments(jexl).context(context)
                    .options(options != null ? options : options(context)).frame(frame);
                final Interpreter interpreter = jexl.createTemplateInterpreter(args);
                return prepare(interpreter);
            } catch (final JexlException xjexl) {
                final JexlException xuel = createException(xjexl.getInfo(), "prepare", this, xjexl);
                if (jexl.isSilent()) {
                    if (logger.isWarnEnabled()) {
                        logger.warn(xuel.getMessage(), xuel.getCause());
                    }
                    return null;
                }
                throw xuel;
            }
        }

        @Override
        public final String toString() {
            final StringBuilder strb = new StringBuilder();
            asString(strb);
            if (source != this) {
                strb.append(" /*= ");
                strb.append(source);
                strb.append(" */");
            }
            return strb.toString();
        }
    }

    /**
     * Helper for expression dealing with embedded strings.
     *
     * @param strb the expression buffer to copy characters into
     * @param expr the source
     * @param position the offset into the source
     * @param c the separator character
     * @return the new position to read the source from
     */
    private static int append(final StringBuilder strb, final CharSequence expr, final int position, final char c) {
        strb.append(c);
        if (c != '"' && c != '\'') {
            return position;
        }
        // read thru strings
        final int end = expr.length();
        boolean escape= false;
        int index = position + 1;
        for (; index < end; ++index) {
            final char ec = expr.charAt(index);
            strb.append(ec);
            if (ec == '\\') {
                escape = !escape;
            } else if (escape) {
                escape = false;
            } else if (ec == c) {
                break;
            }
        }
        return index;
    }

    /**
     * Creates a JxltEngine.Exception from a JexlException.
     *
     * @param info   the source info
     * @param action createExpression, prepare, evaluate
     * @param expr   the template expression
     * @param xany   the exception
     * @return an exception containing an explicit error message
     */
    static Exception createException(final JexlInfo info,
                                     final String action,
                                     final TemplateExpression expr,
                                     final java.lang.Exception xany) {
        final StringBuilder strb = new StringBuilder("failed to ");
        strb.append(action);
        if (expr != null) {
            strb.append(" '");
            strb.append(expr);
            strb.append("'");
        }
        final Throwable cause = xany.getCause();
        if (cause != null) {
            final String causeMsg = cause.getMessage();
            if (causeMsg != null) {
                strb.append(", ");
                strb.append(causeMsg);
            }
        }
        return new Exception(info, strb.toString(), xany);
    }

    /**
     * Reads lines from a (buffered / mark-able) reader keeping all new-lines and line-feeds.
     *
     * @param reader the reader
     * @return the line iterator
     */
    static Iterator<CharSequence> readLines(final Reader reader) {
        if (!reader.markSupported()) {
            throw new IllegalArgumentException("mark support in reader required");
        }
        return new Iterator<CharSequence>() {
            private CharSequence next = doNext();

            private CharSequence doNext() {
                final StringBuilder strb = new StringBuilder(64); // CSOFF: MagicNumber
                int c;
                boolean eol = false;
                try {
                    while ((c = reader.read()) >= 0) {
                        if (eol) {// && (c != '\n' && c != '\r')) {
                            reader.reset();
                            break;
                        }
                        if (c == '\n') {
                            eol = true;
                        }
                        strb.append((char) c);
                        reader.mark(1);
                    }
                } catch (final IOException xio) {
                    return null;
                }
                return strb.length() > 0 ? strb : null;
            }

            @Override
            public boolean hasNext() {
                return next != null;
            }

            @Override
            public CharSequence next() {
                final CharSequence current = next;
                if (current != null) {
                    next = doNext();
                }
                return current;
            }
        };
    }

    /** The TemplateExpression cache. */
    final JexlCache<Source, Object> cache;

    /** The JEXL engine instance. */
    final Engine jexl;

    /** The logger. */
    final Log logger;

    /** The first character for immediate expressions. */
    final char immediateChar;

    /** The first character for deferred expressions. */
    final char deferredChar;

    /** Whether expressions can use JEXL script or only expressions (i.e., no for, var, etc). */
    final boolean noscript;

    /**
     * Creates a new instance of {@link JxltEngine} creating a local cache.
     *
     * @param jexl     the JexlEngine to use.
     * @param noScript  whether this engine only allows JEXL expressions or scripts
     * @param cacheSize the number of expressions in this cache, default is 256
     * @param immediate the immediate template expression character, default is '$'
     * @param deferred  the deferred template expression character, default is '#'
     */
    public TemplateEngine(final Engine jexl,
                          final boolean noScript,
                          final int cacheSize,
                          final char immediate,
                          final char deferred) {
        this.jexl = jexl;
        this.logger = jexl.logger;
        this.cache = jexl.createCache(cacheSize);
        immediateChar = immediate;
        deferredChar = deferred;
        noscript = noScript;
    }

    /**
     * Clears the cache.
     */
    @Override
    public void clearCache() {
        synchronized (cache) {
            cache.clear();
        }
    }

    @Override
    public JxltEngine.Expression createExpression(final JexlInfo jexlInfo, final String expression) {
        return createExpression(jexlInfo, expression, null);
    }

    public JxltEngine.Expression createExpression(final JexlInfo jexlInfo, final String expression, final Scope scope) {
        final JexlInfo info = jexlInfo == null ?  jexl.createInfo() : jexlInfo;
        Exception xuel = null;
        TemplateExpression stmt = null;
        final JexlFeatures features = noscript ? jexl.expressionFeatures : jexl.scriptFeatures;
        // do not cache interpolation expression, they are stored in AST node
        final boolean cached = cache != null && expression.length() < jexl.cacheThreshold;
        try {
            if (!cached) {
                stmt = parseExpression(info, expression, scope);
            } else {
                final Source source = new Source(features, Scope.getSymbolsMap(scope), expression);
                final Object c = cache.get(source);
                stmt = c instanceof TemplateExpression ? (TemplateExpression) c : null;
                if (stmt != null) {
                    return stmt;
                }
                stmt = parseExpression(info, expression, scope);
                cache.put(source, stmt);
            }
        } catch (final JexlException xjexl) {
            xuel = new Exception(xjexl.getInfo(), "failed to parse '" + expression + "'", xjexl);
        }
        if (xuel != null) {
            if (!jexl.isSilent()) {
                throw xuel;
            }
            if (logger.isWarnEnabled()) {
                logger.warn(xuel.getMessage(), xuel.getCause());
            }
            stmt = null;
        }
        return stmt;
    }

    @Override
    public TemplateScript createTemplate(final JexlInfo info, final String prefix, final Reader source, final String... parms) {
        return new TemplateScript(this, info, prefix, source,  parms);
    }

    /**
     * @return the deferred character
     */
    char getDeferredChar() {
        return deferredChar;
    }

    /**
     * Gets the JexlEngine underlying this JxltEngine.
     *
     * @return the JexlEngine
     */
    @Override
    public Engine getEngine() {
        return jexl;
    }

    /**
     * @return the immediate character
     */
    char getImmediateChar() {
        return immediateChar;
    }

    /**
     * Parses a unified expression.
     *
     * @param info  the source info
     * @param expr  the string expression
     * @param scope the template scope
     * @return the unified expression instance
     * @throws JexlException if an error occurs during parsing
     */
    TemplateExpression parseExpression(final JexlInfo info, final String expr, final Scope scope) {  // CSOFF: MethodLength
        final int size = expr.length();
        final ExpressionBuilder builder = new ExpressionBuilder(0);
        final StringBuilder strb = new StringBuilder(size);
        ParseState state = ParseState.CONST;
        int immediate1 = 0;
        int deferred1 = 0;
        int inner1 = 0;
        boolean nested = false;
        int inested = -1;
        int lineno = info.getLine();
        for (int column = 0; column < size; ++column) {
            final char c = expr.charAt(column);
            switch (state) {
            case CONST:
                if (c == immediateChar) {
                    state = ParseState.IMMEDIATE0;
                } else if (c == deferredChar) {
                    inested = column;
                    state = ParseState.DEFERRED0;
                } else if (c == '\\') {
                    state = ParseState.ESCAPE;
                } else {
                    // do buildup expr
                    strb.append(c);
                }
                break;
            case IMMEDIATE0: // $
                if (c != '{') {
                    // revert to CONST
                    strb.append(immediateChar);
                    state = ParseState.CONST;
                    // 'unread' the current character
                    column -= 1;
                    continue;
                }
                state = ParseState.IMMEDIATE1;
                // if chars in buffer, create constant
                if (strb.length() > 0) {
                    final TemplateExpression cexpr = new ConstantExpression(strb.toString(), null);
                    builder.add(cexpr);
                    strb.delete(0, Integer.MAX_VALUE);
                }
                break;
            case DEFERRED0: // #
                if (c != '{') {
                    // revert to CONST
                    strb.append(deferredChar);
                    state = ParseState.CONST;
                    // 'unread' the current character
                    column -= 1;
                    continue;
                }
                state = ParseState.DEFERRED1;
                // if chars in buffer, create constant
                if (strb.length() > 0) {
                    final TemplateExpression cexpr = new ConstantExpression(strb.toString(), null);
                    builder.add(cexpr);
                    strb.delete(0, Integer.MAX_VALUE);
                }
                break;
            case IMMEDIATE1: // ${...
                if (c == '}') {
                    if (immediate1 > 0) {
                        immediate1 -= 1;
                        strb.append(c);
                    } else {
                        // materialize the immediate expr
                        final String src = escapeString(strb);
                        final JexlInfo srcInfo = info.at(lineno, column);
                        final TemplateExpression iexpr = new ImmediateExpression(src,
                            jexl.jxltParse(srcInfo, noscript, src, scope), null);
                        builder.add(iexpr);
                        strb.delete(0, Integer.MAX_VALUE);
                        state = ParseState.CONST;
                    }
                } else if (!isIgnorable(c)) {
                    if (c == '{') {
                        immediate1 += 1;
                    }
                    // do buildup expr
                    column = append(strb, expr, column, c);
                }
                break;
            case DEFERRED1: // #{...
                // skip inner strings - for '}' -
                // nested immediate in deferred; need to balance count of '{' & '}'
                // closing '}'
                switch (c) {
                    case '"':
                    case '\'':
                        strb.append(c);
                        column = StringParser.readString(strb, expr, column + 1, c);
                        continue;
                    case '{':
                        if (expr.charAt(column - 1) == immediateChar) {
                            inner1 += 1;
                            strb.deleteCharAt(strb.length() - 1);
                            nested = true;
                        } else {
                            deferred1 += 1;
                            strb.append(c);
                        }
                        continue;
                    case '}':
                        // balance nested immediate
                        if (deferred1 > 0) {
                            deferred1 -= 1;
                            strb.append(c);
                        } else if (inner1 > 0) {
                            inner1 -= 1;
                        } else {
                            // materialize the nested/deferred expr
                            final String src = escapeString(strb);
                            final JexlInfo srcInfo = info.at(lineno, column);
                            TemplateExpression dexpr;
                            if (nested) {
                                dexpr = new NestedExpression(
                                        escapeString(expr.substring(inested, column + 1)),
                                        jexl.jxltParse(srcInfo, noscript, src, scope),
                                        scope);
                            } else {
                                dexpr = new DeferredExpression(
                                        src,
                                        jexl.jxltParse(srcInfo, noscript, src, scope));
                            }
                            builder.add(dexpr);
                            strb.delete(0, Integer.MAX_VALUE);
                            nested = false;
                            state = ParseState.CONST;
                        }
                        break;
                    default:
                        if (!isIgnorable(c)) {
                            // do buildup expr
                            column = append(strb, expr, column, c);
                        }
                        break;
                }
                break;
            case ESCAPE:
                if (c == deferredChar) {
                    strb.append(deferredChar);
                } else if (c == immediateChar) {
                    strb.append(immediateChar);
                } else {
                    strb.append('\\');
                    strb.append(c);
                }
                state = ParseState.CONST;
                break;
            default: // in case we ever add new unified expression type
                throw new UnsupportedOperationException("unexpected unified expression type");
            }
            if (c == '\n') {
                lineno += 1;
            }
        }
        // we should be in that state
        if (state != ParseState.CONST) {
            // otherwise, we ended a line with a \, $ or #
            switch (state) {
                case ESCAPE:
                    strb.append('\\');
                    strb.append('\\');
                    break;
                case DEFERRED0:
                    strb.append(deferredChar);
                    break;
                case IMMEDIATE0:
                    strb.append(immediateChar);
                    break;
                default:
                    throw new Exception(info.at(lineno, 0), "malformed expression: " + expr, null);
            }
        }
        // if any chars were buffered, add them as a constant
        if (strb.length() > 0) {
            final TemplateExpression cexpr = new ConstantExpression(strb.toString(), null);
            builder.add(cexpr);
        }
        return builder.build(this, null);
    }

    private String escapeString(final CharSequence str) {
        return StringParser.escapeString(str, (char) 0);
    }

    /**
     * Determines whether the given character is considered ignorable whitespace in
     * template expressions.
     * <p>
     * The characters newline ({@code '\n'}), carriage return ({@code '\r'}), tab
     * ({@code '\t'}) and form feed ({@code '\f'}) are treated as ignorable within
     * template expressions and are skipped by the parser. These characters are
     * <em>not</em> ignored between the expression prefix ({@code '$'} or
     * {@code '#'}) and the opening brace {@code '{'}; in that position they
     * influence parsing instead of being discarded.
     * </p>
     *
     * @param c the character to test
     * @return {@code true} if the character is ignorable, {@code false} otherwise
     */
    private static boolean isIgnorable(char c) {
        return c == '\n' || c == '\r' || c == '\t' || c == '\f';
    }

    /**
     * Reads lines of a template grouping them by typed blocks.
     *
     * @param prefix the directive prefix
     * @param source the source reader
     * @return the list of blocks
     */
     List<Block> readTemplate(final String prefix, final Reader source) {
        final ArrayList<Block> blocks = new ArrayList<>();
        final BufferedReader reader;
        if (source instanceof BufferedReader) {
            reader = (BufferedReader) source;
        } else {
            reader = new BufferedReader(source);
        }
        final StringBuilder strb = new StringBuilder();
        BlockType type = null;
        int prefixLen;
        final Iterator<CharSequence> lines = readLines(reader);
        int lineno = 1;
        int start = 0;
        while (lines.hasNext()) {
            final CharSequence line = lines.next();
            if (line == null) {
                break;
            }
            if (type == null) {
                // determine starting type if not known yet
                prefixLen = startsWith(line, prefix);
                if (prefixLen >= 0) {
                    type = BlockType.DIRECTIVE;
                    strb.append(line.subSequence(prefixLen, line.length()));
                } else {
                    type = BlockType.VERBATIM;
                    strb.append(line.subSequence(0, line.length()));
                }
                start = lineno;
            } else if (type == BlockType.DIRECTIVE) {
                // switch to verbatim if necessary
                prefixLen = startsWith(line, prefix);
                if (prefixLen < 0) {
                    final Block directive = new Block(BlockType.DIRECTIVE, start, strb.toString());
                    strb.delete(0, Integer.MAX_VALUE);
                    blocks.add(directive);
                    type = BlockType.VERBATIM;
                    strb.append(line.subSequence(0, line.length()));
                    start = lineno;
                } else {
                    // still a directive
                    strb.append(line.subSequence(prefixLen, line.length()));
                }
            } else { //if (type == BlockType.VERBATIM) {
                // switch to directive if necessary
                prefixLen = startsWith(line, prefix);
                if (prefixLen >= 0) {
                    final Block verbatim = new Block(BlockType.VERBATIM, start, strb.toString());
                    strb.delete(0, Integer.MAX_VALUE);
                    blocks.add(verbatim);
                    type = BlockType.DIRECTIVE;
                    strb.append(line.subSequence(prefixLen, line.length()));
                    start = lineno;
                } else {
                    strb.append(line.subSequence(0, line.length()));
                }
            }
            lineno += 1;
        }
        // input may be null
        if (type != null && strb.length() > 0) {
            final Block block = new Block(type, start, strb.toString());
            blocks.add(block);
        }
        blocks.trimToSize();
        return blocks;
    }

    /**
     * Tests whether a sequence starts with a given set of characters (following spaces).
     * <p>Space characters at beginning of line before the pattern are discarded.</p>
     *
     * @param sequence the sequence
     * @param pattern  the pattern to match at start of sequence
     * @return the first position after end of pattern if it matches, -1 otherwise
     */
    int startsWith(final CharSequence sequence, final CharSequence pattern) {
        final int length = sequence.length();
        int s = 0;
        while (s < length && Character.isSpaceChar(sequence.charAt(s))) {
            s += 1;
        }
        if (s < length && pattern.length() <= length - s) {
            final CharSequence subSequence = sequence.subSequence(s, length);
            if (subSequence.subSequence(0, pattern.length()).equals(pattern)) {
                return s + pattern.length();
            }
        }
        return -1;
    }
}
