/*
 * 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.JexlContext;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.JexlInfo;
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 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.Set;

/**
 * A JxltEngine implementation.
 * @since 3.0
 */
public final class TemplateEngine extends JxltEngine {
    /** The TemplateExpression cache. */
    private final SoftCache<String, TemplateExpression> cache;
    /** The JEXL engine instance. */
    private final Engine jexl;
    /** The first character for immediate expressions. */
    private final char immediateChar;
    /** The first character for deferred expressions. */
    private final char deferredChar;
    /** Whether expressions can use JEXL script or only expressions (ie, no for, var, etc). */
    private boolean noscript = true;

    /**
     * Creates a new instance of {@link JxltEngine} creating a local cache.
     * @param aJexl     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(Engine aJexl, boolean noScript, int cacheSize, char immediate, char deferred) {
        this.jexl = aJexl;
        this.cache = new SoftCache<String, TemplateExpression>(cacheSize);
        immediateChar = immediate;
        deferredChar = deferred;
        noscript = noScript;
    }

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

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

    /**
     * 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(int idx) {
            this.index = idx;
        }
    }

    /**
     * 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 ArrayList<TemplateExpression> expressions;

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

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

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

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

        /**
         * Builds an TemplateExpression from a source, performs checks.
         * @param el     the unified el instance
         * @param source the source TemplateExpression
         * @return an TemplateExpression
         */
        private TemplateExpression build(TemplateEngine el, TemplateExpression source) {
            int sum = 0;
            for (int count : counts) {
                sum += count;
            }
            if (expressions.size() != sum) {
                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);
            } else {
                return el.new CompositeExpression(counts, expressions, source);
            }
        }
    }

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

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

    /**
     * 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(TemplateExpression src) {
            this.source = src != null ? src : this;
        }

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

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

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

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

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

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

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

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

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

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

        /**
         * Prepares this expression.
         * @param frame the frame storing parameters and local variables
         * @param context the context storing global variables
         * @return the expression value
         * @throws JexlException
         */
        protected final TemplateExpression prepare(Scope.Frame frame, JexlContext context) {
            try {
                Interpreter interpreter = new TemplateInterpreter(jexl, context, frame, null, null);
                return prepare(interpreter);
            } catch (JexlException xjexl) {
                JexlException xuel = createException(xjexl.getInfo(), "prepare", this, xjexl);
                if (jexl.isSilent()) {
                    jexl.logger.warn(xuel.getMessage(), xuel.getCause());
                    return null;
                }
                throw xuel;
            }
        }

        /**
         * 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(Interpreter interpreter) {
            return this;
        }

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

        /**
         * Evaluates this expression.
         * @param frame the frame storing parameters and local variables
         * @param context the context storing global variables
         * @return the expression value
         * @throws JexlException
         */
        protected final Object evaluate(Scope.Frame frame, JexlContext context) {
            try {
                Interpreter interpreter = new TemplateInterpreter(jexl, context, frame, null, null);
                return evaluate(interpreter);
            } catch (JexlException xjexl) {
                JexlException xuel = createException(xjexl.getInfo(), "evaluate", this, xjexl);
                if (jexl.isSilent()) {
                    jexl.logger.warn(xuel.getMessage(), xuel.getCause());
                    return null;
                }
                throw xuel;
            }
        }

        /**
         * 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);

    }

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

        /**
         * Creates a constant unified expression.
         * <p>
         * If the wrapped constant is a string, it is treated
         * as a JEXL strings with respect to escaping.
         * </p>
         * @param val    the constant value
         * @param source the source TemplateExpression if any
         */
        ConstantExpression(Object val, TemplateExpression source) {
            super(source);
            if (val == null) {
                throw new NullPointerException("constant can not be null");
            }
            if (val instanceof String) {
                val = StringParser.buildString((String) val, false);
            }
            this.value = val;
        }

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

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

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

    /** 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(CharSequence theExpr, JexlNode theNode, TemplateExpression theSource) {
            super(theSource);
            this.expr = theExpr;
            this.node = theNode;
        }

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

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

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

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

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

    /** An immediate unified expression: ${jexl}. */
    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(CharSequence expr, JexlNode node, TemplateExpression source) {
            super(expr, node, source);
        }

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

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

    /** A deferred unified expression: #{jexl}. */
    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
         * @param source the source unified expression if any
         */
        DeferredExpression(CharSequence expr, JexlNode node, TemplateExpression source) {
            super(expr, node, source);
        }

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

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

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

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

    /**
     * An immediate unified expression nested into a deferred unified expression.
     * #{...${jexl}...}
     * Note that the deferred syntax is JEXL's.
     */
    class NestedExpression extends JexlBasedExpression {
        /**
         * Creates a nested 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
         */
        NestedExpression(CharSequence expr, JexlNode node, TemplateExpression source) {
            super(expr, node, source);
            if (this.source != this) {
                throw new IllegalArgumentException("Nested TemplateExpression can not have a source");
            }
        }

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

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

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

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

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

    /** A composite unified expression: "... ${...} ... #{...} ...". */
    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. */
        protected 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 expresion if any
         */
        CompositeExpression(int[] counters, ArrayList<TemplateExpression> list, TemplateExpression src) {
            super(src);
            this.exprs = list.toArray(new TemplateExpression[list.size()]);
            this.meta = (counters[ExpressionType.DEFERRED.index] > 0 ? 2 : 0)
                    | (counters[ExpressionType.IMMEDIATE.index] > 0 ? 1 : 0);
        }

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

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

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

        @Override
        public Set<List<String>> getVariables() {
            Engine.VarCollector collector = new Engine.VarCollector();
            for (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(Engine.VarCollector collector) {
            for (TemplateExpression expr : exprs) {
                expr.getVariables(collector);
            }
        }

        @Override
        protected TemplateExpression prepare(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 (int e = 0; e < size; ++e) {
                TemplateExpression expr = exprs[e];
                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);
        }

        @Override
        protected Object evaluate(Interpreter interpreter) {
            final int size = exprs.length;
            Object value;
            // common case: evaluate all expressions & concatenate them as a string
            StringBuilder strb = new StringBuilder();
            for (int e = 0; e < size; ++e) {
                value = exprs[e].evaluate(interpreter);
                if (value != null) {
                    strb.append(value.toString());
                }
            }
            value = strb.toString();
            return value;
        }
    }


    @Override
    public JxltEngine.Expression createExpression(JexlInfo info, String expression) {
        if (info == null) {
            info = jexl.createInfo();
        }
        Exception xuel = null;
        TemplateExpression stmt = null;
        try {
            stmt = cache.get(expression);
            if (stmt == null) {
                stmt = parseExpression(info, expression, null);
                cache.put(expression, stmt);
            }
        } catch (JexlException xjexl) {
            xuel = new Exception(xjexl.getInfo(), "failed to parse '" + expression + "'", xjexl);
        }
        if (xuel != null) {
            if (jexl.isSilent()) {
                jexl.logger.warn(xuel.getMessage(), xuel.getCause());
                stmt = null;
            } else {
                throw xuel;
            }
        }
        return stmt;
    }

    /**
     * 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(JexlInfo info, String action, TemplateExpression expr, java.lang.Exception xany) {
        StringBuilder strb = new StringBuilder("failed to ");
        strb.append(action);
        if (expr != null) {
            strb.append(" '");
            strb.append(expr.toString());
            strb.append("'");
        }
        Throwable cause = xany.getCause();
        if (cause != null) {
            String causeMsg = cause.getMessage();
            if (causeMsg != null) {
                strb.append(", ");
                strb.append(causeMsg);
            }
        }
        return new Exception(info, strb.toString(), xany);
    }

    /** The different parsing states. */
    private enum ParseState {
        /** Parsing a constant. */
        CONST,
        /** Parsing after $ . */
        IMMEDIATE0,
        /** Parsing after # . */
        DEFERRED0,
        /** Parsing after ${ . */
        IMMEDIATE1,
        /** Parsing after #{ . */
        DEFERRED1,
        /** Parsing after \ . */
        ESCAPE
    }

    /**
     * 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 occur during parsing
     */
    TemplateExpression parseExpression(JexlInfo info, String expr, 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) {
            char c = expr.charAt(column);
            switch (state) {
                default: // in case we ever add new unified expresssion type
                    throw new UnsupportedOperationException("unexpected unified expression type");
                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 == '{') {
                        state = ParseState.IMMEDIATE1;
                        // if chars in buffer, create constant
                        if (strb.length() > 0) {
                            TemplateExpression cexpr = new ConstantExpression(strb.toString(), null);
                            builder.add(cexpr);
                            strb.delete(0, Integer.MAX_VALUE);
                        }
                    } else {
                        // revert to CONST
                        strb.append(immediateChar);
                        strb.append(c);
                        state = ParseState.CONST;
                    }
                    break;
                case DEFERRED0: // #
                    if (c == '{') {
                        state = ParseState.DEFERRED1;
                        // if chars in buffer, create constant
                        if (strb.length() > 0) {
                            TemplateExpression cexpr = new ConstantExpression(strb.toString(), null);
                            builder.add(cexpr);
                            strb.delete(0, Integer.MAX_VALUE);
                        }
                    } else {
                        // revert to CONST
                        strb.append(deferredChar);
                        strb.append(c);
                        state = ParseState.CONST;
                    }
                    break;
                case IMMEDIATE1: // ${...
                    if (c == '}') {
                        if (immediate1 > 0) {
                            immediate1 -= 1;
                            strb.append(c);
                        } else {
                            // materialize the immediate expr
                            String src = strb.toString();
                            TemplateExpression iexpr = new ImmediateExpression(
                                    src,
                                    jexl.parse(info.at(lineno, column), src, scope, false, noscript),
                                    null);
                            builder.add(iexpr);
                            strb.delete(0, Integer.MAX_VALUE);
                            state = ParseState.CONST;
                        }
                    } else {
                        if (c == '{') {
                            immediate1 += 1;
                        }
                        // do buildup expr
                        strb.append(c);
                    }
                    break;
                case DEFERRED1: // #{...
                    // skip inner strings (for '}')
                    if (c == '"' || c == '\'') {
                        strb.append(c);
                        column = StringParser.readString(strb, expr, column + 1, c);
                        continue;
                    }
                    // nested immediate in deferred; need to balance count of '{' & '}'
                    if (c == '{') {
                        if (expr.charAt(column - 1) == immediateChar) {
                            inner1 += 1;
                            strb.deleteCharAt(strb.length() - 1);
                            nested = true;
                        } else {
                            deferred1 += 1;
                            strb.append(c);
                        }
                        continue;
                    }
                    // closing '}'
                    if (c == '}') {
                        // balance nested immediate
                        if (deferred1 > 0) {
                            deferred1 -= 1;
                            strb.append(c);
                        } else if (inner1 > 0) {
                            inner1 -= 1;
                        } else  {
                            // materialize the nested/deferred expr
                            String src = strb.toString();
                            TemplateExpression dexpr;
                            if (nested) {
                                dexpr = new NestedExpression(
                                        expr.substring(inested, column + 1),
                                        jexl.parse(info.at(lineno, column), src, scope, false, noscript),
                                        null);
                            } else {
                                dexpr = new DeferredExpression(
                                        strb.toString(),
                                        jexl.parse(info.at(lineno, column), src, scope, false, noscript),
                                        null);
                            }
                            builder.add(dexpr);
                            strb.delete(0, Integer.MAX_VALUE);
                            nested = false;
                            state = ParseState.CONST;
                        }
                    } else {
                        // do buildup expr
                        strb.append(c);
                    }
                    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;
            }
            if (c == '\n') {
                lineno += 1;
            }
        }
        // we should be in that state
        if (state != ParseState.CONST) {
            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) {
            TemplateExpression cexpr = new ConstantExpression(strb.toString(), null);
            builder.add(cexpr);
        }
        return builder.build(this, null);
    }

    /**
     * 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, ie a fragment of JEXL code. */
        DIRECTIVE
    }

    /**
     * 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(BlockType theType, int theLine, String theBlock) {
            type = theType;
            line = theLine;
            body = theBlock;
        }

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

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

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

        @Override
        public String toString() {
            if (BlockType.VERBATIM.equals(type)) {
                return body;
            } else {
                // CHECKSTYLE:OFF
                StringBuilder strb = new StringBuilder(64); // CSOFF: MagicNumber
                // CHECKSTYLE:ON
                Iterator<CharSequence> lines = readLines(new StringReader(body));
                while (lines.hasNext()) {
                    strb.append("$$").append(lines.next());
                }
                return strb.toString();
            }
        }

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



    /**
     * 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
     */
    protected int startsWith(CharSequence sequence, CharSequence pattern) {
        int length = sequence.length();
        int s = 0;
        while (s < length && Character.isSpaceChar(sequence.charAt(s))) {
            s += 1;
        }
        if (s < length && pattern.length() <= (length - s)) {
            sequence = sequence.subSequence(s, length);
            if (sequence.subSequence(0, pattern.length()).equals(pattern)) {
                return s + pattern.length();
            }
        }
        return -1;
    }

    /**
     * Read lines from a (buffered / mark-able) reader keeping all new-lines and line-feeds.
     * @param reader the reader
     * @return the line iterator
     */
    protected 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() {
                StringBuffer strb = new StringBuffer(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 (IOException xio) {
                    return null;
                }
                return strb.length() > 0 ? strb : null;
            }

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

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

            @Override
            public void remove() {
                throw new UnsupportedOperationException("Not supported.");
            }
        };
    }

    /**
     * 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
     */
    protected List<Block> readTemplate(final String prefix, Reader source) {
        final ArrayList<Block> blocks = new ArrayList<Block>();
        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;
        Iterator<CharSequence> lines = readLines(reader);
        int lineno = 0;
        int start = 0;
        while (lines.hasNext()) {
            CharSequence line = lines.next();
            if (line == null) {
                break;
            } else 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) {
                    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) {
                    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) {
            Block block = new Block(type, start, strb.toString());
            blocks.add(block);
        }
        blocks.trimToSize();
        return blocks;
    }

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