/*
 * Copyright 2015 Martin Kouba
 *
 * Licensed 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.trimou.handlebars;

import static org.trimou.handlebars.OptionsHashKeys.OPERATOR;
import static org.trimou.handlebars.OptionsHashKeys.OUTPUT;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.function.BiConsumer;

import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;
import org.trimou.handlebars.HelperDefinition.ValuePlaceholder;

/**
 * A simple numeric expression helper. During evaluation all the params are
 * converted to {@link BigDecimal}s. For the list of supported operators see the
 * {@link Operator} enum values, e.g.:
 *
 *

 * {{numExpr val op="neg" out='It is a negative number!'}}
 * {{#numExpr val "90" op="gt"}}
 *  val > 90
 * {{/numExpr}}
 * {{#numExpr val 10 op="eq"}}
 *  val == 10
 * {{/numExpr}}
 * {{#numExpr val1 val2 op="le"}}
 *  val1 <= val2
 * {{/numExpr}}
 * {{#numExpr val "1" 2 '3' op="in"}}
 *  val = 1 or val = 2 or val = 3
 * {{/numExpr}}
 *

 *
 *

 * It's also possible to specify the default operator ({@link Operator#EQ} by
 * default) so that the op param may be ommitted:
 *

 *
 *

 * {{#gt val1 10}} val1 > 10 {{/gt}}
 *

 *
 *

 * Sometimes it also makes sense to register the helper with a name derived from
 * the default operator or even register a helper instance for each operator -
 * see {@link #forEachOperator()}.
 *

 *
 * @author Martin Kouba
 */
public class NumericExpressionHelper extends BasicHelper {

    private final Operator defaultOperator;

    /**
     * {@link Operator#toString()} is used as the helper name.
     *
     * @return a new builder instance with {@link NumericExpressionHelper} instance
     *         registered for each {@link Operator}
     */
    public static HelpersBuilder forEachOperator() {
        return forEachOperator(HelpersBuilder.empty());
    }

    /**
     *
     * @param builder
     * @return an enriched builder instance with {@link NumericExpressionHelper} instance
     *         registered for each {@link Operator}
     */
    public static HelpersBuilder forEachOperator(HelpersBuilder builder) {
        forEachOperator((name, helper) -> builder.add(name, helper));
        return builder;
    }

    /**
     * Invokes the specified consumer for all operators. {@link Operator#toString()}
     * is used as the first argument and the corresponding
     * {@link NumericExpressionHelper} instance as the second argument.
     *
     *
     * @param consumer
     */
    public static void forEachOperator(BiConsumer<String, Helper> consumer) {
        for (Operator operator : Operator.values()) {
            consumer.accept(operator.toString().toLowerCase(), new NumericExpressionHelper(operator));
        }
    }

    /**
     *
     */
    public NumericExpressionHelper() {
        this(Operator.EQ);
    }

    /**
     *
     * @param defaultOperator
     */
    public NumericExpressionHelper(Operator defaultOperator) {
        this.defaultOperator = defaultOperator;
    }

    @Override
    public void execute(Options options) {

        Operator operator = initOperator(options);

        if (operator.getMinParams() > options.getParameters().size()) {
            // We need this check because the operator may be set dynamically
            throw new MustacheException(
                    MustacheProblem.RENDER_HELPER_INVALID_OPTIONS,
                    "More parameters required [helper: %s, template: %s, line: %s]",
                    NumericExpressionHelper.class.getName(),
                    options.getTagInfo().getTemplateName(),
                    options.getTagInfo().getLine());
        }

        boolean result = operator.evaluate(options);

        if (result) {
            if (isSection(options)) {
                options.fn();
            } else {
                String output;
                Object outputValue = options.getHash().get(OUTPUT);
                output = outputValue != null ? convertValue(outputValue)
                        : Boolean.TRUE.toString();
                append(options, output);
            }
        }
    }

    @Override
    public void validate(HelperDefinition definition) {
        super.validate(definition);
        Operator operator;
        Object value = definition.getHash().get(OPERATOR);
        if (value == null) {
            operator = Operator.EQ;
        } else if (value instanceof ValuePlaceholder) {
            // Operator set dynamically
            operator = null;
        } else {
            operator = Operator.from(value.toString());
        }
        if (operator != null && operator.getMinParams() > definition
                .getParameters().size()) {
            throw new MustacheException(
                    MustacheProblem.COMPILE_HELPER_VALIDATION_FAILURE,
                    "Operator requires more parameters [helper: %s, template: %s, line: %s]",
                    this.getClass().getName(),
                    definition.getTagInfo().getTemplateName(),
                    definition.getTagInfo().getLine());
        }
    }

    private Operator initOperator(Options options) {
        Operator operator = null;
        Object value = options.getHash().get(OPERATOR);
        if (value != null) {
            operator = Operator.from(value.toString());
        }
        return operator != null ? operator : defaultOperator;
    }

    private static BigDecimal getDecimal(int index, Options options) {
        return getDecimal(options.getParameters().get(index), options);
    }

    static BigDecimal getDecimal(Object value, Options options) {
        BigDecimal decimal;
        if (value instanceof BigDecimal) {
            decimal = (BigDecimal) value;
        } else if (value instanceof BigInteger) {
            decimal = new BigDecimal((BigInteger) value);
        } else if (value instanceof Long) {
            decimal = new BigDecimal((Long) value);
        } else if (value instanceof Integer) {
            decimal = new BigDecimal((Integer) value);
        } else if (value instanceof Double) {
            decimal = new BigDecimal((Double) value);
        } else if (value instanceof String) {
            decimal = new BigDecimal(value.toString());
        } else {
            throw new MustacheException(
                    MustacheProblem.RENDER_HELPER_INVALID_OPTIONS,
                    "Parameter is not valid [param: %s, helper: %s, template: %s, line: %s]",
                    value, NumericExpressionHelper.class.getName(),
                    options.getTagInfo().getTemplateName(),
                    options.getTagInfo().getLine());
        }
        return decimal;
    }

    /**
     *
     * @author Martin Kouba
     */
    public enum Operator {

        /**
         * Evaluates to true if the first and the second value are equal in
         * value.
         *
         * @see BigDecimal#compareTo(BigDecimal)
         */
        EQ(new EqualsEvaluator()),
        /**
         * Evaluates to true if the first and the second value are NOT equal in
         * value.
         *
         * @see BigDecimal#compareTo(BigDecimal)
         */
        NEQ(new InverseEvaluator(new EqualsEvaluator())),
        /**
         * Evaluates to true if the first value is greater than the second
         * value.
         *
         * @see BigDecimal#compareTo(BigDecimal)
         */
        GT(options -> {
            BigDecimal val1 = getDecimal(0, options);
            BigDecimal val2 = getDecimal(1, options);
            return val1.compareTo(val2) > 0;
        }),
        /**
         * Evaluates to true if the first value is greater than or equal to the
         * second value.
         *
         * @see BigDecimal#compareTo(BigDecimal)
         */
        GE(options -> {
            BigDecimal val1 = getDecimal(0, options);
            BigDecimal val2 = getDecimal(1, options);
            return val1.compareTo(val2) >= 0;
        }),
        /**
         * Evaluates to true if the first value is less than the second value.
         *
         * @see BigDecimal#compareTo(BigDecimal)
         */
        LT(options -> {
            BigDecimal val1 = getDecimal(0, options);
            BigDecimal val2 = getDecimal(1, options);
            return val1.compareTo(val2) < 0;
        }),
        /**
         * Evaluates to true if the first value is less than or equal to the
         * second value.
         *
         * @see BigDecimal#compareTo(BigDecimal)
         */
        LE(options -> {
            BigDecimal val1 = getDecimal(0, options);
            BigDecimal val2 = getDecimal(1, options);
            return val1.compareTo(val2) <= 0;
        }),
        /**
         * Evaluates to true if the first value is negative.
         */
        NEG(1, options -> getDecimal(0, options).compareTo(BigDecimal.ZERO) < 0),
        /**
         * Evaluates to true if the first value is positive.
         */
        POS(1, options -> getDecimal(0, options).compareTo(BigDecimal.ZERO) > 0),
        /**
         * Evaluates to true if the first value is found in the set of other
         * values. Elements of {@link Iterable}s and arrays are treated as
         * separate objects.
         */
        IN(new InEvaluator()),
        /**
         * Evaluates to true if the first value is not found in the set of other
         * values. Elements of {@link Iterable}s and arrays are treated as
         * separate objects.
         */
        NIN(new InverseEvaluator(new InEvaluator())),;

        Operator(Evaluator evaluator) {
            this(2, evaluator);
        }

        Operator(int minParams, Evaluator evaluator) {
            this.minParams = minParams;
            this.evaluator = evaluator;
        }

        private final int minParams;

        private final Evaluator evaluator;

        public int getMinParams() {
            return minParams;
        }

        public boolean evaluate(Options options) {
            return evaluator.evaluate(options);
        }

        static Operator from(String value) {
            if (value != null) {
                for (Operator operator : values()) {
                    if (operator.toString().equalsIgnoreCase(value)) {
                        return operator;
                    }
                }
            }
            return null;
        }

    }

    interface Evaluator {

        boolean evaluate(Options options);
    }

    private static final class InEvaluator implements Evaluator {

        @Override
        public boolean evaluate(Options options) {
            BigDecimal val = getDecimal(0, options);
            for (int i = 1; i < options.getParameters().size(); i++) {
                Object toTest = options.getParameters().get(i);
                if (toTest == null) {
                    continue;
                }
                if (toTest instanceof Iterable) {
                    for (final Object o : ((Iterable<?>) toTest)) {
                        if (test(val, getDecimal(o, options))) {
                            return true;
                        }
                    }
                } else if (toTest.getClass().isArray()) {
                    int length = Array.getLength(toTest);
                    for (int j = 0; j < length; j++) {
                        if (test(val,
                                getDecimal(Array.get(toTest, j), options))) {
                            return true;
                        }
                    }
                } else {
                    if (test(val, getDecimal(toTest, options))) {
                        return true;
                    }
                }
            }
            return false;
        }

        private boolean test(BigDecimal val1, BigDecimal val2) {
            return val1.compareTo(val2) == 0;
        }

    }

    private static class EqualsEvaluator implements Evaluator {

        @Override
        public boolean evaluate(Options options) {
            BigDecimal val1 = getDecimal(0, options);
            BigDecimal val2 = getDecimal(1, options);
            return val1.compareTo(val2) == 0;
        }

    }

    private static class InverseEvaluator implements Evaluator {

        private final Evaluator evaluator;

        InverseEvaluator(Evaluator evaluator) {
            this.evaluator = evaluator;
        }

        @Override
        public boolean evaluate(Options options) {
            return !evaluator.evaluate(options);
        }

    }

}


/*
 * Copyright 2013 Martin Kouba
 *
 * Licensed 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.trimou.engine.parser;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.engine.parser.DefaultParsingHandler.ContainerSegmentBase;
import org.trimou.engine.parser.DefaultParsingHandler.PartialSegmentBase;
import org.trimou.engine.parser.DefaultParsingHandler.RootSegmentBase;
import org.trimou.engine.parser.DefaultParsingHandler.SegmentBase;
import org.trimou.engine.segment.SegmentType;
import org.trimou.util.Strings;

/**
 * {@link SegmentBase} utils.
 *
 * @author Martin Kouba
 */
final class SegmentBases {

    private static final Logger LOGGER = LoggerFactory
            .getLogger(SegmentBases.class);

    private SegmentBases() {
    }

    static void removeStandaloneLines(RootSegmentBase rootSegment) {

        List<List<SegmentBase>> lines = readSegmentLines(rootSegment);

        // First identify the segments to remove
        Set<SegmentBase> segmentsToRemove = new HashSet<>();
        int idx = 0;
        for (List<SegmentBase> line : lines) {
            idx++;
            if (isStandaloneLine(line)) {

                // Extract indentation for standalone partial segment
                extractIndentationForPartial(line);

                for (SegmentBase segment : line) {
                    if (segment instanceof ContainerSegmentBase
                            || SegmentType.PARTIAL.equals(segment.getType())) {
                        continue;
                    }
                    segmentsToRemove.add(segment);
                }
                LOGGER.trace("Segment line {} is standalone", idx);
            }
        }

        // Then remove segments
        if (!segmentsToRemove.isEmpty()) {
            removeSegments(segmentsToRemove, rootSegment);
            LOGGER.debug("{} segments removed", segmentsToRemove.size());
        }

    }

    static void removeUnnecessarySegments(ContainerSegmentBase container) {

        for (Iterator<SegmentBase> iterator = container.iterator(); iterator
                .hasNext();) {
            SegmentBase segment = iterator.next();
            if (segment instanceof ContainerSegmentBase) {
                removeUnnecessarySegments((ContainerSegmentBase) segment);
            } else if (SegmentType.COMMENT.equals(segment.getType())
                    || SegmentType.DELIMITERS.equals(segment.getType())) {
                iterator.remove();
            }
        }
    }

    static void reuseLineSeparatorSegments(ContainerSegmentBase container) {

        Map<String, SegmentBase> lineSeparators = new HashMap<>();

        for (ListIterator<SegmentBase> iterator = container.listIterator(); iterator
                .hasNext();) {

            SegmentBase segment = iterator.next();

            if (segment instanceof ContainerSegmentBase) {
                reuseLineSeparatorSegments((ContainerSegmentBase) segment);
            } else if (SegmentType.LINE_SEPARATOR.equals(segment.getType())) {
                SegmentBase lineSeparator = lineSeparators.get(segment
                        .getContent());
                if (lineSeparator == null) {
                    lineSeparators.put(segment.getContent(), segment);
                } else {
                    iterator.set(lineSeparator);
                }
            }
        }
    }

    /**
     *
     * @param standaloneLine
     */
    private static void extractIndentationForPartial(
            List<SegmentBase> standaloneLine) {

        if (SegmentType.TEXT.equals(standaloneLine.get(0).getType())) {

            // First segment is whitespace - try to find partial
            PartialSegmentBase partial = null;
            for (SegmentBase segment : standaloneLine) {
                if (SegmentType.PARTIAL.equals(segment.getType())) {
                    partial = (PartialSegmentBase) segment;
                    break;
                }
            }

            if (partial != null) {
                partial.setIndentation(standaloneLine.get(0).getContent());
            }
        }
    }

    /**
     *
     * @param segmentsToRemove
     * @param container
     */
    private static void removeSegments(Set<SegmentBase> segmentsToRemove,
            ContainerSegmentBase container) {

        for (Iterator<SegmentBase> iterator = container.iterator(); iterator
                .hasNext();) {
            SegmentBase segment = iterator.next();
            if (segment instanceof ContainerSegmentBase) {
                removeSegments(segmentsToRemove, (ContainerSegmentBase) segment);
            } else {
                if (segmentsToRemove.contains(segment)) {
                    iterator.remove();
                }
            }
        }
    }

    /**
     *
     * @param line
     * @return
     */
    static boolean isStandaloneLine(List<SegmentBase> line) {

        boolean standaloneCandidate = false;

        for (SegmentBase segment : line) {
            if (SegmentType.VALUE.equals(segment.getType())) {
                // Value segment
                return false;
            } else if (SegmentType.TEXT.equals(segment.getType())) {
                if (!Strings.containsOnlyWhitespace(segment.getContent())) {
                    // Text segment with non-whitespace character
                    return false;
                }
            } else if (isStandaloneCandidate(segment.getType())) {
                standaloneCandidate = true;
            }
        }
        return standaloneCandidate;
    }

    private static boolean isStandaloneCandidate(SegmentType type) {
        return type.equals(SegmentType.COMMENT)
                || type.equals(SegmentType.SECTION)
                || type.equals(SegmentType.INVERTED_SECTION)
                || type.equals(SegmentType.DELIMITERS)
                || type.equals(SegmentType.PARTIAL);
    }

    /**
     * Read segment lines recursively.
     *
     * @param container
     * @return
     */
    private static List<List<SegmentBase>> readSegmentLines(
            ContainerSegmentBase container) {
        List<List<SegmentBase>> lines = new ArrayList<>();
        // Add the last line manually - there is no line separator to trigger
        // flush
        lines.add(readSegmentLines(lines, null, container));
        return lines;
    }

    /**
     *
     * @param lines
     * @param currentLine
     * @param container
     * @return
     */
    private static List<SegmentBase> readSegmentLines(
            List<List<SegmentBase>> lines, List<SegmentBase> currentLine,
            ContainerSegmentBase container) {

        if (currentLine == null) {
            currentLine = new ArrayList<>();
        }

        if (!SegmentType.ROOT.equals(container.getType())) {
            // Simulate the start tag
            currentLine.add(container);
        }

        for (SegmentBase segment : container) {
            if (segment instanceof ContainerSegmentBase) {
                currentLine = readSegmentLines(lines, currentLine,
                        (ContainerSegmentBase) segment);
            } else if (!SegmentType.LINE_SEPARATOR.equals(segment.getType())) {
                currentLine.add(segment);
            } else {
                // New line separator - flush the line
                currentLine.add(segment);
                lines.add(currentLine);
                currentLine = new ArrayList<>();
            }
        }

        if (!SegmentType.ROOT.equals(container.getType())) {
            // Simulate the end tag
            currentLine.add(container);
        }
        return currentLine;
    }

}


/*
 * Copyright 2013 Martin Kouba
 *
 * Licensed 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.trimou.util;

import java.util.regex.Pattern;

import org.trimou.engine.config.Configuration;
import org.trimou.engine.config.EngineConfigurationKey;

/**
 *
 * @author Martin Kouba
 */
public final class Patterns {

    private Patterns() {
    }

    /**
     * Delimiters are quoted to avoid regexp reserved characters conflict.
     *
     * @param configuration
     * @return the new delimiters pattern
     */
    public static Pattern newMustacheTagPattern(Configuration configuration) {
        StringBuilder regex = new StringBuilder();
        regex.append(Pattern.quote(configuration
                .getStringPropertyValue(EngineConfigurationKey.START_DELIMITER)));
        regex.append(".*?");
        regex.append(Pattern.quote(configuration
                .getStringPropertyValue(EngineConfigurationKey.END_DELIMITER)));
        return Pattern.compile(regex.toString());
    }

    /**
     * Useful to extract start and end delimiters from "set delimiters" tag
     * content, e.g. <% %> (without equals signs).
     *
     * First and third groups contain one or more non-whitespace characters.
     * Second group contains one or more whitespace characters.
     *
     * Originally we used the following pattern:
     * ([[^=]&&\\S]+)(\\s+)([[^=]&&\\S]+).
     *
     * @return the pattern to match new delimiters in "set delimiters" tag
     */
    public static Pattern newSetDelimitersContentPattern() {
        return Pattern.compile("(\\S+)(\\s+)(\\S+)");
    }

    /**
     *
     * @return  the pattern to match an integer literal in a helper name
     */
    public static Pattern newHelperIntegerLiteralPattern() {
        return Pattern.compile("(\\+|-)?\\d{1,10}");
    }

    /**
    *
    * @return  the pattern to match a long literal in a helper name
    */
   public static Pattern newHelperLongLiteralPattern() {
       return Pattern.compile("(\\+|-)?\\d{1,19}(L|l)");
   }

}


/*
 * Copyright 2018 Trimou team
 *
 * Licensed 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.trimou.engine.resolver;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Function;
import java.util.stream.Stream;

import org.trimou.Mustache;
import org.trimou.engine.MustacheEngine;
import org.trimou.engine.config.Configuration;
import org.trimou.exception.MustacheException;
import org.trimou.util.Checker;
import org.trimou.util.ImmutableMap;

/**
 * This util class can be used to decorate a delegate instance, e.g. to add a
 * computed property to a context object (data) passed to
 * {@link Mustache#render(Object)}. The following snippet would render: "ooF".
 *
 *

 * MustacheEngine engine = MustacheEngineBuilder.newBuilder().build();
 * engine.compileMustache("{{reverse}}")
 *         .render(decorate("Foo").compute("reverse", s -> new StringBuilder(s).reverse().toString()).build(engine));
 *

 *
 *

 * Note that {@link ReflectionResolver} and {@link MapResolver} must be
 * registered in order to make it work.
 *

 *
 * @author Martin Kouba
 * @see Builder
 */
public class Decorator<T> implements Mapper {

    public static final String KEY_GET_DELEGATE = "delegate";

    /**
     * Returns a decorator builder for the specified delegate.
     *
     * @param delegate
     * @return a new builder instance
     * @see Builder#build(MustacheEngine)
     */
    public static <T> Builder<T> decorate(T delegate) {
        return new Builder<T>(delegate);
    }

    /**
     * Returns a decorator instance for the specified delegate and mappings.
     *
     * @param delegate
     * @param mappings
     * @param delegateKey
     *            May be null
     * @param configuration
     * @return a new decorator instance
     */
    public static <T> Decorator<T> decorate(T delegate, Map<String, Function<T, Object>> mappings, String delegateKey,
            Configuration configuration) {
        return IterableDecorator.isIterable(delegate)
                ? new IterableDecorator<>(delegate, ImmutableMap.copyOf(mappings), delegateKey, configuration)
                : new Decorator<>(delegate, ImmutableMap.copyOf(mappings), delegateKey, configuration);
    }

    /**
     * This method is recursive.
     *
     * @return the underlying delegate instance
     */
    @SuppressWarnings("unchecked")
    public static <T> T unwrap(T instance) {
        return instance instanceof Decorator ? unwrap(((Decorator<T>) instance).delegate) : instance;
    }

    private final String delegateKey;

    protected final T delegate;

    private final ReflectionResolver reflectionResolver;

    private final Map<String, Function<T, Object>> mappings;

    /**
     *
     * @param delegate
     * @param mappings
     * @param configuration
     */
    private Decorator(T delegate, Map<String, Function<T, Object>> mappings, String delegateKey,
            Configuration configuration) {
        this.delegate = delegate;
        this.reflectionResolver = getReflectionResolver(configuration);
        this.mappings = mappings;
        this.delegateKey = delegateKey != null ? delegateKey : KEY_GET_DELEGATE;
    }

    @Override
    public Object get(String key) {
        if (delegateKey.equals(key)) {
            return delegate;
        } else {
            Function<T, Object> mapping = mappings.get(key);
            if (mapping != null) {
                return mapping.apply(unwrap(delegate));
            } else if (delegate instanceof Mapper) {
                return ((Mapper) delegate).get(key);
            } else {
                return reflectionResolver.resolve(delegate, key, null);
            }
        }
    }

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

    private ReflectionResolver getReflectionResolver(Configuration configuration) {
        for (Resolver resolver : configuration.getResolvers()) {
            if (resolver instanceof ReflectionResolver) {
                return (ReflectionResolver) resolver;
            }
        }
        throw new IllegalStateException("Decorator can only be used if ReflectionResolver is available");
    }

    public static class Builder<T> extends AbstractBuilder<T, Builder<T>> {

        private final T delegate;

        private Builder(T delegate) {
            super();
            Checker.checkArgumentNotNull(delegate);
            this.delegate = delegate;
        }

        @Override
        protected Builder<T> self() {
            return this;
        }

        /**
         *
         * @param engine
         * @return a new decorator instance
         * @throws IllegalStateException
         *             If {@link ReflectionResolver} is not available
         */
        public Decorator<T> build(MustacheEngine engine) {
            return build(engine.getConfiguration());
        }

        /**
         *
         * @param engine
         * @return a new decorator instance
         * @throws IllegalStateException
         *             If {@link ReflectionResolver} is not available
         */
        public Decorator<T> build(Configuration configuration) {
            Checker.checkArgumentNotNull(configuration);
            return Decorator.decorate(delegate, ImmutableMap.copyOf(mappings), delegateKey, configuration);
        }

    }

    /**
     * This version is used for delegates that are arrays or implement an iterable
     * type.
     *
     * @param <T>
     */
    private static class IterableDecorator<T> extends Decorator<T> implements Iterable<Object> {

        static boolean isIterable(Object delegate) {
            return delegate instanceof Iterable || delegate.getClass().isArray() || delegate instanceof Iterator
                    || delegate instanceof Spliterator || delegate instanceof Stream;
        }

        private IterableDecorator(T delegate, Map<String, Function<T, Object>> mappings, String delegateKey,
                Configuration configuration) {
            super(delegate, mappings, delegateKey, configuration);
        }

        @SuppressWarnings({ "unchecked", "rawtypes" })
        @Override
        public Iterator<Object> iterator() {
            if (delegate instanceof Iterable) {
                return ((Iterable) delegate).iterator();
            } else if (delegate.getClass().isArray()) {
                // This is not very effective
                int length = Array.getLength(delegate);
                List<Object> elements = new ArrayList<>(length);
                for (int i = 0; i < length; i++) {
                    elements.add(Array.get(delegate, i));
                }
                return elements.iterator();
            } else if (delegate instanceof Iterator) {
                return (Iterator) delegate;
            } else if (delegate instanceof Spliterator) {
                return Spliterators.iterator((Spliterator<?>) delegate);
            } else if (delegate instanceof Stream) {
                return ((Stream) delegate).sequential().iterator();
            } else {
                throw new MustacheException(delegate + "is not iterable");
            }
        }

    }

    /**
     * Logic shared accross various decorator-related builders.
     *
     * @param <T>
     * @param
     */
    public abstract static class AbstractBuilder<T, B extends AbstractBuilder<T, B>> {

        protected String delegateKey;

        protected final Map<String, Function<T, Object>> mappings;

        public AbstractBuilder() {
            this.mappings = new HashMap<>();
        }

        /**
         * Associates the specified value with the specified key.
         *
         * @param key
         * @param value
         * @return self
         */
        public B put(String key, Object value) {
            return compute(key, delegate -> value);
        }

        /**
         * Associates the specified mapping function with the specified key. The input
         * to the function is the delegate instance. The function is applied each time a
         * value for the given key is requested.
         *
         * @param key
         * @param mapper
         * @return self
         */
        public B compute(String key, Function<T, Object> mapper) {
            Checker.checkArgumentsNotNull(key, mapper);
            mappings.put(key, mapper);
            return self();
        }

        /**
         * Associates the specified mapping function with the specified key. The input
         * to the function is the delegate instance. The function is only applied once -
         * when a value for the given key is requested the result is cached.
         *
         * @param key
         * @param mapper
         * @return self
         */
        public B computeOnce(String key, Function<T, Object> mapper) {
            Checker.checkArgumentsNotNull(key, mapper);
            mappings.put(key, new Function<T, Object>() {

                private volatile Object value;

                @Override
                public Object apply(T delegate) {
                    Object ret = value;
                    if (ret == null) {
                        synchronized (this) {
                            ret = value;
                            if (ret == null) {
                                ret = mapper.apply(delegate);
                                value = ret;
                            }
                        }
                    }
                    return ret;
                }
            });
            return self();
        }

        /**
         * The specified key can be used to obtain the undelying delegate instance.
         *
         * @param key
         * @return self
         * @see Decorator#KEY_GET_DELEGATE
         */
        public B delegateKey(String key) {
            this.delegateKey = key;
            return self();
        }

        protected abstract B self();

    }

}


/*
 * Copyright 2013 Martin Kouba
 *
 * Licensed 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.trimou.engine;

import static org.trimou.util.Checker.checkArgumentNotNull;
import static org.trimou.util.Checker.checkArgumentsNotNull;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutorService;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.Mustache;
import org.trimou.engine.cache.ComputingCacheFactory;
import org.trimou.engine.config.ConfigurationAware;
import org.trimou.engine.config.ConfigurationExtension;
import org.trimou.engine.config.ConfigurationExtension.ConfigurationExtensionBuilder;
import org.trimou.engine.config.ConfigurationKey;
import org.trimou.engine.convert.ContextConverter;
import org.trimou.engine.convert.ValueConverter;
import org.trimou.engine.id.IdentifierGenerator;
import org.trimou.engine.interpolation.KeySplitter;
import org.trimou.engine.interpolation.LiteralSupport;
import org.trimou.engine.interpolation.MissingValueHandler;
import org.trimou.engine.listener.MustacheListener;
import org.trimou.engine.locale.LocaleSupport;
import org.trimou.engine.locator.TemplateLocator;
import org.trimou.engine.resolver.Resolver;
import org.trimou.engine.text.TextSupport;
import org.trimou.handlebars.Helper;
import org.trimou.util.ImmutableList;
import org.trimou.util.ImmutableMap;
import org.trimou.util.ImmutableSet;

/**
 * A builder for {@link MustacheEngine}. It's not thread-safe. The builder is
 * considered immutable once the {@link #build()} method is called. Subsequent
 * invocations of any modifying method or {@link #build()} result in an
 * {@link IllegalStateException}.
 *

 * Note that most {@link ConfigurationAware} components are tied to the specific
 * engine instance and cannot be reused as well.
 *
 * @author Martin Kouba
 */
public final class MustacheEngineBuilder
        implements ConfigurationExtensionBuilder {

    private static final Logger LOGGER = LoggerFactory
            .getLogger(MustacheEngineBuilder.class);

    private boolean isBuilt;

    private boolean omitServiceLoaderConfigurationExtensions;

    private final Set<Resolver> resolvers;

    private final Set<TemplateLocator> templateLocators;

    private final Map<String, Object> globalData;

    private TextSupport textSupport;

    private LocaleSupport localeSupport;

    private final Map<String, Object> properties;

    private final List<EngineBuiltCallback> engineReadyCallbacks;

    private final List<MustacheListener> mustacheListeners;

    private KeySplitter keySplitter;

    private MissingValueHandler missingValueHandler;

    private final Map<String, Helper> helpers;

    private ComputingCacheFactory computingCacheFactory;

    private IdentifierGenerator identifierGenerator;

    private ExecutorService executorService;

    private LiteralSupport literalSupport;

    private ClassLoader configurationExtensionClassLoader;

    private final Set<ValueConverter> valueConverters;

    private final Set<ContextConverter> contextConverters;

    /**
     * Don't create a new instance.
     *
     * @see #newBuilder()
     */
    private MustacheEngineBuilder() {
        this.omitServiceLoaderConfigurationExtensions = false;
        this.resolvers = new HashSet<>();
        this.templateLocators = new HashSet<>();
        this.globalData = new HashMap<>();
        this.properties = new HashMap<>();
        this.mustacheListeners = new ArrayList<>();
        this.helpers = new HashMap<>();
        this.engineReadyCallbacks = new ArrayList<>();
        this.valueConverters = new HashSet<>();
        this.contextConverters = new HashSet<>();
    }

    /**
     * Builds the engine instance.
     *
     * @return the built engine
     */
    public synchronized MustacheEngine build() {
        MustacheEngine engine = new DefaultMustacheEngine(this);
        for (EngineBuiltCallback callback : engineReadyCallbacks) {
            callback.engineBuilt(engine);
        }
        BuildInfo buildInfo = BuildInfo.load();
        LOGGER.info("Engine built {} ({})", buildInfo.getVersion(), buildInfo.getTimestampDate());
        LOGGER.debug("Engine configuration: {}",
                engine.getConfiguration().getInfo());
        isBuilt = true;
        return engine;
    }

    /**
     * Adds a value (e.g. Lambda) that is available during execution of all
     * templates.
     *
     * Global values have to be thread-safe.
     *
     * @param value
     * @param name
     * @return self
     */
    public MustacheEngineBuilder addGlobalData(String name, Object value) {
        checkArgumentsNotNull(name, value);
        checkNotBuilt();
        this.globalData.put(name, value);
        return this;
    }

    /**
     * Adds a template locator.
     *
     * @param locator
     * @return self
     */
    public MustacheEngineBuilder addTemplateLocator(TemplateLocator locator) {
        checkArgumentNotNull(locator);
        checkNotBuilt();
        this.templateLocators.add(locator);
        return this;
    }

    /**
     * Adds a value resolver.
     *
     * @param resolver
     * @return self
     */
    public MustacheEngineBuilder addResolver(Resolver resolver) {
        checkArgumentNotNull(resolver);
        checkNotBuilt();
        this.resolvers.add(resolver);
        return this;
    }

    /**
     * Sets a configuration property.
     *
     * @param key
     * @param value
     * @return self
     */
    public MustacheEngineBuilder setProperty(String key, Object value) {
        checkArgumentsNotNull(key, value);
        checkNotBuilt();
        this.properties.put(key, value);
        return this;
    }

    /**
     * Sets a configuration property.
     *
     * @param configurationKey
     * @param value
     * @param <T>
     *            The type of configuration key
     * @return self
     */
    public <T extends ConfigurationKey> MustacheEngineBuilder setProperty(
            T configurationKey, Object value) {
        checkArgumentsNotNull(configurationKey, value);
        checkNotBuilt();
        setProperty(configurationKey.get(), value);
        return this;
    }

    /**
     * Sets a text support instance.
     *
     * @param textSupport
     * @return self
     */
    public MustacheEngineBuilder setTextSupport(TextSupport textSupport) {
        checkArgumentNotNull(textSupport);
        checkNotBuilt();
        this.textSupport = textSupport;
        return this;
    }

    /**
     * Sets a locale support instance.
     *
     * @param localeSupport
     * @return self
     */
    public MustacheEngineBuilder setLocaleSupport(LocaleSupport localeSupport) {
        checkArgumentNotNull(localeSupport);
        checkNotBuilt();
        this.localeSupport = localeSupport;
        return this;
    }

    /**
     * Callback is useful to configure a component instantiated before the
     * engine is built.
     *
     * @param callback
     * @return self
     */
    public MustacheEngineBuilder registerCallback(
            EngineBuiltCallback callback) {
        checkArgumentNotNull(callback);
        checkNotBuilt();
        this.engineReadyCallbacks.add(callback);
        return this;
    }

    /**
     * Adds a {@link Mustache} listener. Manually added listeners are always
     * registered before listeners added via configuration extensions.
     *
     * @param listener
     * @return self
     */
    public MustacheEngineBuilder addMustacheListener(
            MustacheListener listener) {
        checkArgumentNotNull(listener);
        checkNotBuilt();
        this.mustacheListeners.add(listener);
        return this;
    }

    /**
     *
     * @param keySplitter
     * @return self
     */
    public MustacheEngineBuilder setKeySplitter(KeySplitter keySplitter) {
        checkArgumentNotNull(keySplitter);
        checkNotBuilt();
        this.keySplitter = keySplitter;
        return this;
    }

    /**
     *
     * @param missingValueHandler
     * @return self
     */
    public MustacheEngineBuilder setMissingValueHandler(
            MissingValueHandler missingValueHandler) {
        checkArgumentNotNull(missingValueHandler);
        checkNotBuilt();
        this.missingValueHandler = missingValueHandler;
        return this;
    }

    /**
     * Each helper must be registered with a unique name. If there are more
     * helpers registered with the same name an {@link IllegalArgumentException}
     * is thrown. Use {@link #registerHelper(String, Helper, boolean)} to
     * overwrite the helper.
     *
     * @param name
     * @param helper
     * @return self
     */
    public MustacheEngineBuilder registerHelper(String name, Helper helper) {
        return registerHelper(name, helper, false);
    }

    /**
     * Each helper must be registered with a unique name. If there is a helper
     * registered with the same name and the param overwrite is
     * true the previous instance is replaced, otherwise an
     * {@link IllegalArgumentException} is thrown.
     *
     * @param name
     * @param helper
     * @param overwrite
     * @return self
     */
    public MustacheEngineBuilder registerHelper(String name, Helper helper,
            boolean overwrite) {
        checkArgumentsNotNull(name, helper);
        checkNotBuilt();
        if (!overwrite && helpers.containsKey(name)) {
            throw new IllegalArgumentException(
                    "A helper with this name is already registered: " + name);
        }
        helpers.put(name, helper);
        return this;
    }

    /**
     * Each helper must be registered with a unique name. If there are more
     * helpers registered with the same name an {@link IllegalArgumentException}
     * is thrown. Use {@link #registerHelpers(Map, boolean)} to overwrite the
     * helpers.
     *
     * @param helpers
     * @return self
     */
    public MustacheEngineBuilder registerHelpers(Map<String, Helper> helpers) {
        return registerHelpers(helpers, false);
    }

    /**
     * Each helper must be registered with a unique name. If there is a helper
     * registered with the same name and the param overwrite is
     * true the previous instance is replaced, otherwise an
     * {@link IllegalArgumentException} is thrown.
     *
     * @param helpers
     * @param overwrite
     * @return
     */
    public MustacheEngineBuilder registerHelpers(Map<String, Helper> helpers,
            boolean overwrite) {
        checkArgumentNotNull(helpers);
        checkNotBuilt();
        for (Entry<String, Helper> entry : helpers.entrySet()) {
            registerHelper(entry.getKey(), entry.getValue(), overwrite);
        }
        return this;
    }

    /**
     * Don't use the ServiceLoader mechanism to load configuration extensions
     * (i.e. the default resolvers are not added automatically).
     *
     * @see ConfigurationExtension
     */
    public MustacheEngineBuilder omitServiceLoaderConfigurationExtensions() {
        checkNotBuilt();
        this.omitServiceLoaderConfigurationExtensions = true;
        return this;
    }

    /**
     * Set the custom {@link ComputingCacheFactory}.
     *
     * @param cacheFactory
     * @return self
     */
    public MustacheEngineBuilder setComputingCacheFactory(
            ComputingCacheFactory cacheFactory) {
        checkArgumentNotNull(cacheFactory);
        checkNotBuilt();
        this.computingCacheFactory = cacheFactory;
        return this;
    }

    /**
     * Set the custom {@link IdentifierGenerator}.
     *
     * @param identifierGenerator
     * @return self
     */
    public MustacheEngineBuilder setIdentifierGenerator(
            IdentifierGenerator identifierGenerator) {
        checkArgumentNotNull(identifierGenerator);
        checkNotBuilt();
        this.identifierGenerator = identifierGenerator;
        return this;
    }

    /**
     * Set the {@link ExecutorService} to be used for async tasks.
     *
     * @param executorService
     * @return self
     */
    public MustacheEngineBuilder setExecutorService(
            ExecutorService executorService) {
        checkArgumentNotNull(executorService);
        checkNotBuilt();
        this.executorService = executorService;
        return this;
    }

    /**
     * Set the custom {@link LiteralSupport}.
     *
     * @param literalSupport
     * @return self
     */
    public MustacheEngineBuilder setLiteralSupport(
            LiteralSupport literalSupport) {
        checkArgumentNotNull(literalSupport);
        checkNotBuilt();
        this.literalSupport = literalSupport;
        return this;
    }

    /**
     * Set the {@link ClassLoader} used to load {@link ConfigurationExtension}s.
     *
     * @param configurationExtensionClassLoader
     * @return self
     */
    public MustacheEngineBuilder setConfigurationExtensionClassLoader(
            ClassLoader configurationExtensionClassLoader) {
        checkArgumentNotNull(configurationExtensionClassLoader);
        checkNotBuilt();
        this.configurationExtensionClassLoader = configurationExtensionClassLoader;
        return this;
    }

    /**
     * Add a value converter.
     *
     * @param converter
     * @return self
     */
    public MustacheEngineBuilder addValueConverter(ValueConverter converter) {
        checkArgumentNotNull(converter);
        checkNotBuilt();
        this.valueConverters.add(converter);
        return this;
    }

    /**
     * Add a context converter.
     *
     * @param converter
     * @return self
     */
    public MustacheEngineBuilder addContextConverter(ContextConverter converter) {
        checkArgumentNotNull(converter);
        checkNotBuilt();
        this.contextConverters.add(converter);
        return this;
    }

    /**
     *
     * @return a new instance of builder
     */
    public static MustacheEngineBuilder newBuilder() {
        return new MustacheEngineBuilder();
    }

    /**
     *
     * @author Martin Kouba
     * @see MustacheEngineBuilder#registerCallback(EngineBuiltCallback)
     */
    @FunctionalInterface
    public interface EngineBuiltCallback {

        void engineBuilt(MustacheEngine engine);

    }

    public Set<TemplateLocator> buildTemplateLocators() {
        return ImmutableSet.copyOf(templateLocators);
    }

    public Set<Resolver> buildResolvers() {
        return ImmutableSet.copyOf(resolvers);
    }

    public Map<String, Object> buildGlobalData() {
        return ImmutableMap.copyOf(globalData);
    }

    public TextSupport getTextSupport() {
        return textSupport;
    }

    public LocaleSupport getLocaleSupport() {
        return localeSupport;
    }

    public boolean isOmitServiceLoaderConfigurationExtensions() {
        return omitServiceLoaderConfigurationExtensions;
    }

    public Map<String, Object> buildProperties() {
        return ImmutableMap.copyOf(properties);
    }

    public List<MustacheListener> buildMustacheListeners() {
        return ImmutableList.copyOf(mustacheListeners);
    }

    public KeySplitter getKeySplitter() {
        return keySplitter;
    }

    public MissingValueHandler getMissingValueHandler() {
        return missingValueHandler;
    }

    public Map<String, Helper> buildHelpers() {
        return ImmutableMap.copyOf(helpers);
    }

    public ComputingCacheFactory getComputingCacheFactory() {
        return computingCacheFactory;
    }

    public IdentifierGenerator getIdentifierGenerator() {
        return identifierGenerator;
    }

    public ExecutorService getExecutorService() {
        return executorService;
    }

    public LiteralSupport getLiteralSupport() {
        return literalSupport;
    }

    public ClassLoader getConfigurationExtensionClassLoader() {
        return configurationExtensionClassLoader;
    }

    public Set<ValueConverter> buildValueConverters() {
        return ImmutableSet.copyOf(valueConverters);
    }

    public Set<ContextConverter> buildContextConverters() {
        return ImmutableSet.copyOf(contextConverters);
    }

    private void checkNotBuilt() {
        if (isBuilt) {
            throw new IllegalStateException(
                    "Invalid method invocation - builder already built!");
        }
    }

}


/*
 * Copyright 2013 Martin Kouba
 *
 * Licensed 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.trimou.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;

import org.trimou.annotations.Internal;

/**
 *
 * @author Martin Kouba
 */
@Internal
public final class Strings {

    public static final String EMPTY = "";

    public static final String GAP = " ";

    public static final String LINE_SEPARATOR = System
            .getProperty("line.separator");

    /**
     * Linux, BDS, etc.
     */
    public static final String LINE_SEPARATOR_LF = "\n";

    /**
     * DOS, OS/2, Microsoft Windows, etc.
     */
    public static final String LINE_SEPARATOR_CRLF = "\r\n";

    /**
     * Mac OS 9, ZX Spectrum :-), etc.
     */
    public static final String LINE_SEPARATOR_CR = "\r";

    public static final String SLASH = "/";

    public static final String FILE_SEPARATOR = System
            .getProperty("file.separator");

    public static final String DOT = ".";

    public static final String UNDERSCORE = "_";

    public static final String NOT_AVAILABLE = "N/A";

    public static final String URL_PROCOTOL_FILE = "file";

    public static final String THIS = "this";

    public static final String HASH = "#";

    private Strings() {
    }

    /**
     *
     * @param character
     * @return true if the char is a string literal separator,
     *         false otherwise
     */
    public static boolean isStringLiteralSeparator(char character) {
        return character == '"' || character == '\'';
    }

    /**
     *
     * @param sequence
     * @return true if the given sequence is null or empty
     */
    public static boolean isEmpty(CharSequence sequence) {
        return sequence == null || sequence.length() == 0;
    }

    /**
     *
     * @param word
     * @return the uncapitalized input
     */
    public static String uncapitalize(String word) {
        if (isEmpty(word)) {
            return word;
        }
        StringBuilder builder = new StringBuilder();
        builder.append(Character.toLowerCase(word.charAt(0)));
        if (word.length() > 1) {
            builder.append(word.substring(1, word.length()));
        }
        return builder.toString();
    }

    /**
     * Capitalizes all the delimiter separated words.
     *
     * @param text
     * @param delimiter
     * @return the capitalized input
     */
    public static String capitalizeFully(String text, Character delimiter) {
        if (isEmpty(text)) {
            return text;
        }
        text = text.toLowerCase();
        boolean capitalizeNext = true;
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < text.length(); i++) {
            final char ch = text.charAt(i);
            if (delimiter.equals(ch)) {
                capitalizeNext = true;
                builder.append(ch);
            } else if (capitalizeNext) {
                builder.append(Character.toTitleCase(ch));
                capitalizeNext = false;
            } else {
                builder.append(ch);
            }
        }
        return builder.toString();
    }

    /**
     *
     * @param text
     * @param search
     * @param replacement
     * @return the processed text
     */
    public static String replace(String text, String search,
            String replacement) {
        if (isEmpty(text) || isEmpty(search) || replacement == null) {
            return text;
        }
        int start = 0;
        int end = text.indexOf(search, start);
        if (end == -1) {
            return text;
        }
        StringBuilder builder = new StringBuilder();
        while (end != -1) {
            builder.append(text.substring(start, end));
            builder.append(replacement);
            start = end + search.length();
            end = text.indexOf(search, start);
        }
        builder.append(text.substring(start));
        return builder.toString();
    }

    /**
     *
     * @param text
     * @param suffix
     * @return the text without the suffix, if the text ends with the suffix
     */
    public static String removeSuffix(String text, String suffix) {
        if (isEmpty(text) || isEmpty(suffix)) {
            return text;
        }
        if (text.endsWith(suffix)) {
            return text.substring(0, text.length() - suffix.length());
        }
        return text;
    }

    /**
     *
     * @param text
     * @param delimiter
     * @return a list of tokens
     */
    public static List<String> split(String text, String delimiter) {
        if (isEmpty(text) || isEmpty(delimiter)) {
            return Collections.singletonList(text);
        }
        List<String> tokens = new ArrayList<>();
        StringTokenizer tokenizer = new StringTokenizer(text, delimiter);
        while (tokenizer.hasMoreTokens()) {
            tokens.add(tokenizer.nextToken());
        }
        return Collections.unmodifiableList(tokens);
    }

    /**
     *
     * @param text
     * @return true if the given text contains a whitespace
     *         character
     */
    public static boolean containsWhitespace(String text) {
        if (isEmpty(text)) {
            return false;
        }
        for (int i = 0; i < text.length(); i++) {
            if (Character.isWhitespace(text.charAt(i))) {
                return true;
            }
        }
        return false;
    }

    /**
     *
     * @param text
     * @return true if the given text contains only whitespace
     *         characters
     */
    public static boolean containsOnlyWhitespace(String text) {
        if (isEmpty(text)) {
            return false;
        }
        for (int i = 0; i < text.length(); i++) {
            if (!Character.isWhitespace(text.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     *
     * @param text
     * @return true if the given text contains only digits
     */
    public static boolean containsOnlyDigits(String text) {
        if (isEmpty(text)) {
            return false;
        }
        for (int i = 0; i < text.length(); i++) {
            if (!Character.isDigit(text.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     *
     * @param text
     * @return a new String consisting of the input repeated the specified
     *         number of times
     */
    public static String repeat(String text, int times, String separator) {
        if (isEmpty(text) || times < 0) {
            return text;
        }
        if (times == 0) {
            return EMPTY;
        }
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < times; i++) {
            builder.append(text);
            if (i + 1 < times) {
                builder.append(separator);
            }
        }
        return builder.toString();
    }

    /**
     *
     * @param text
     * @param delimiter
     * @return the substring after the first delimiter or an empty string if
     *         nothing is found
     */
    public static String substringAfter(String text, String delimiter) {
        Checker.checkArgumentsNotNull(text, delimiter);
        int index = text.indexOf(delimiter);
        if (index == -1) {
            return EMPTY;
        }
        return text.substring(index + delimiter.length());
    }

    public static boolean isListLiteralStart(char character) {
        return character == '[';
    }

    public static boolean isListLiteralEnd(char character) {
        return character == ']';
    }

    public static boolean isListLiteral(String value) {
        return isListLiteralStart(value.charAt(0)) && isListLiteralEnd(value.charAt(value.length() - 1));
    }

}


/*
 * Copyright 2013 Martin Kouba
 *
 * Licensed 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.trimou.handlebars;

import java.io.IOException;

import org.trimou.engine.config.AbstractConfigurationAware;
import org.trimou.engine.config.EngineConfigurationKey;
import org.trimou.engine.convert.ValueConverter;
import org.trimou.engine.text.TextSupport;
import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;

/**
 * An abstract helper that holds a reference to the {@link TextSupport}.
 *
 * @author Martin Kouba
 */
public abstract class AbstractHelper extends AbstractConfigurationAware
        implements Helper {

    private volatile TextSupport textSupport;

    @Override
    protected void init() {
        if (!configuration.getBooleanPropertyValue(
                EngineConfigurationKey.SKIP_VALUE_ESCAPING)) {
            textSupport = configuration.getTextSupport();
        }
    }

    protected boolean isSection(Options options) {
        return Helpers.isSection(options);
    }

    protected boolean isVariable(Options options) {
        return Helpers.isVariable(options);
    }

    protected boolean isUnescapeVariable(Options options) {
        return Helpers.isUnescapeVariable(options);
    }

    /**
     * Escape appended sequence if needed.
     *
     * @param options
     * @param sequence
     * @see TextSupport
     */
    protected void append(Options options, CharSequence sequence) {
        TextSupport textSupport = this.textSupport;
        if (textSupport == null || isUnescapeVariable(options)) {
            options.append(sequence);
        } else {
            try {
                textSupport.appendEscapedHtml(sequence.toString(),
                        options.getAppendable());
            } catch (IOException e) {
                throw new MustacheException(MustacheProblem.RENDER_IO_ERROR, e);
            }
        }
    }

    /**
     *
     * @param value
     * @return the converted value
     * @see ValueConverter
     */
    protected String convertValue(Object value) {
        if (!configuration.getValueConverters().isEmpty()) {
            for (ValueConverter converter : configuration
                    .getValueConverters()) {
                String result = converter.convert(value);
                if (result != null) {
                    return result;
                }
            }
        }
        return value.toString();
    }

    /**
     *
     * @param options
     * @param value
     * @see AbstractHelper#append(Options, CharSequence)
     * @see #convertValue(Object)
     */
    protected void convertAndAppend(Options options, Object value) {
        append(options, convertValue(value));
    }

}


/*
 * Copyright 2013 Martin Kouba
 *
 * Licensed 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.trimou.engine;

import static org.trimou.util.Checker.checkArgumentNotEmpty;
import static org.trimou.util.Checker.checkArgumentNotNull;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.Mustache;
import org.trimou.engine.cache.ComputingCache;
import org.trimou.engine.config.Configuration;
import org.trimou.engine.config.ConfigurationFactory;
import org.trimou.engine.config.EngineConfigurationKey;
import org.trimou.engine.listener.MustacheCompilationEvent;
import org.trimou.engine.listener.MustacheListener;
import org.trimou.engine.listener.MustacheParsingEvent;
import org.trimou.engine.locator.TemplateLocator;
import org.trimou.engine.parser.ParserFactory;
import org.trimou.engine.parser.ParsingHandler;
import org.trimou.engine.parser.ParsingHandlerFactory;
import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;
import org.trimou.util.IOUtils;

/**
 * The default Mustache engine implementation.
 *
 * @author Martin Kouba
 */
class DefaultMustacheEngine implements MustacheEngine {

    private static final Logger LOGGER = LoggerFactory
            .getLogger(DefaultMustacheEngine.class);

    private final ComputingCache<String, Optional<Mustache>> templateCache;

    private final ComputingCache<String, Optional<String>> sourceCache;

    private final Configuration configuration;

    private final ParserFactory parserFactory;

    private final ParsingHandlerFactory parsingHandlerFactory;

    /**
     * Workaround for CDI (JSR 299, JSR 346) - make this type proxyable so that
     * it's possible to produce an application-scoped CDI bean.
     */
    DefaultMustacheEngine() {
        configuration = null;
        parserFactory = null;
        parsingHandlerFactory = null;
        templateCache = null;
        sourceCache = null;
    }

    /**
     *
     * @param builder
     */
    DefaultMustacheEngine(MustacheEngineBuilder builder) {
        // First create the engine configuration
        configuration = new ConfigurationFactory().createConfiguration(builder);
        parserFactory = new ParserFactory();
        parsingHandlerFactory = new ParsingHandlerFactory();

        if (configuration
                .getBooleanPropertyValue(EngineConfigurationKey.DEBUG_MODE)) {
            templateCache = null;
            sourceCache = null;
            LOGGER.warn(
                    "Attention! Debug mode enabled: template cache disabled, additional logging enabled");
        } else {
            if (configuration.getBooleanPropertyValue(
                    EngineConfigurationKey.TEMPLATE_CACHE_ENABLED)) {
                templateCache = buildTemplateCache();
                sourceCache = configuration.getBooleanPropertyValue(
                        EngineConfigurationKey.TEMPLATE_CACHE_USED_FOR_SOURCE)
                                ? buildSourceCache() : null;
                if (configuration.getBooleanPropertyValue(
                        EngineConfigurationKey.PRECOMPILE_ALL_TEMPLATES)) {
                    precompileTemplates();
                }
            } else {
                templateCache = null;
                sourceCache = null;
                LOGGER.info("Template cache explicitly disabled!");
            }
        }
    }

    public Mustache getMustache(String templateId) {
        checkArgumentNotEmpty(templateId);
        return templateCache != null ? getTemplateFromCache(templateId)
                : locateAndParse(templateId);
    }

    public String getMustacheSource(String templateId) {
        checkArgumentNotEmpty(templateId);
        return sourceCache != null ? getSourceFromCache(templateId)
                : locateAndRead(templateId);
    }

    public Mustache compileMustache(String templateId, String templateContent) {
        checkArgumentNotEmpty(templateId);
        checkArgumentNotEmpty(templateContent);
        return parse(templateId, new StringReader(templateContent));
    }

    @Override
    public Mustache compileMustache(String templateContent) {
        return compileMustache(UUID.randomUUID().toString(), templateContent);
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public void invalidateTemplateCache() {
        if (isCacheEnabled()) {
            templateCache.clear();
            if (sourceCache != null) {
                sourceCache.clear();
            }
        }
    }

    @Override
    public void invalidateTemplateCache(Predicate<String> predicate) {
        if (isCacheEnabled()) {
            checkArgumentNotNull(predicate);
            templateCache.invalidate(predicate::test);
            if (sourceCache != null) {
                sourceCache.invalidate(predicate::test);
            }
        }
    }

    private boolean isCacheEnabled() {
        if (templateCache == null) {
            LOGGER.warn(
                    "Unable to invalidate the template cache - it's disabled!");
            return false;
        }
        return true;
    }

    private ComputingCache<String, Optional<Mustache>> buildTemplateCache() {
        return buildCache("Template",
                key ->
                        Optional.ofNullable(locateAndParse(key)),
                (key, cause) ->
                        LOGGER.debug("Removed template from cache [templateId: {}, cause: {}]", key, cause));
    }

    /**
     * Properties of the source cache are dependent on that of the template
     * cache.
     */
    private ComputingCache<String, Optional<String>> buildSourceCache() {
        return buildCache("Source",
                key ->
                        Optional.ofNullable(locateAndRead(key)),
                (key, cause) ->
                        LOGGER.debug("Removed template source from cache [templateId: {}, cause: {}]", key, cause));
    }

    private <K, V> ComputingCache<K, V> buildCache(String name,
            ComputingCache.Function<K, V> loader,
            ComputingCache.Listener<K> listener) {

        Long expirationTimeout = configuration.getLongPropertyValue(
                EngineConfigurationKey.TEMPLATE_CACHE_EXPIRATION_TIMEOUT);

        if (expirationTimeout > 0) {
            LOGGER.info("{} cache expiration timeout set: {} seconds", name, expirationTimeout);
            expirationTimeout = expirationTimeout * 1000L;
        } else {
            expirationTimeout = null;
        }
        return configuration.getComputingCacheFactory().create(
                MustacheEngine.COMPUTING_CACHE_CONSUMER_ID, loader,
                expirationTimeout, null, listener);
    }

    private void precompileTemplates() {
        Set<String> templateNames = new HashSet<>();
        for (TemplateLocator locator : configuration.getTemplateLocators()) {
            templateNames.addAll(locator.getAllIdentifiers());
        }
        for (String templateName : templateNames) {
            getTemplateFromCache(templateName);
        }
    }

    private Mustache parse(String templateId, Reader reader) {
        ParsingHandler handler = parsingHandlerFactory.createParsingHandler();
        reader = notifyListenersBeforeParsing(templateId, reader);
        parserFactory.createParser(this).parse(templateId, reader, handler);
        Mustache mustache = handler.getCompiledTemplate();
        notifyListenersAfterCompilation(mustache);
        return mustache;
    }

    private Reader locate(String templateId) {
        List<TemplateLocator> locators = configuration.getTemplateLocators();
        if (locators == null || locators.isEmpty()) {
            return null;
        }
        Reader reader = null;
        for (TemplateLocator locator : locators) {
            reader = locator.locate(templateId);
            if (reader != null) {
                break;
            }
        }
        return reader;
    }

    private Mustache locateAndParse(String templateId) {
        Reader reader = null;
        try {
            reader = locate(templateId);
            if (reader == null) {
                return null;
            }
            return parse(templateId, reader);
        } finally {
            closeReader(reader, templateId);
        }
    }

    private String locateAndRead(String templateId) {
        Reader reader = null;
        try {
            reader = locate(templateId);
            if (reader == null) {
                return null;
            }
            return IOUtils.toString(reader);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
            return null;
        } finally {
            closeReader(reader, templateId);
        }
    }

    private void closeReader(Reader reader, String templateId) {
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                LOGGER.warn("Unable to close the reader for " + templateId, e);
            }
        }
    }

    private Reader notifyListenersBeforeParsing(String templateName,
            Reader reader) {
        if (configuration.getMustacheListeners() != null) {
            MustacheParsingEvent event = new DefaultMustacheParsingEvent(
                    templateName, reader);
            for (MustacheListener listener : configuration
                    .getMustacheListeners()) {
                listener.parsingStarted(event);
            }
            return event.getMustacheContents();
        }
        return reader;
    }

    private void notifyListenersAfterCompilation(Mustache mustache) {
        if (configuration.getMustacheListeners() != null) {
            MustacheCompilationEvent event = new DefaultMustacheCompilationEvent(
                    mustache);
            for (MustacheListener listener : configuration
                    .getMustacheListeners()) {
                listener.compilationFinished(event);
            }
        }
    }

    private Mustache getTemplateFromCache(String templateName) {
        try {
            return templateCache.get(templateName).orElse(null);
        } catch (Exception e) {
            throw unwrapUncheckedExecutionException(e);
        }
    }

    private String getSourceFromCache(String templateName) {
        try {
            return sourceCache.get(templateName).orElse(null);
        } catch (Exception e) {
            throw unwrapUncheckedExecutionException(e);
        }
    }

    private RuntimeException unwrapUncheckedExecutionException(Exception e) {
        Throwable cause = e.getCause() == null ? e : e.getCause();
        if (cause instanceof RuntimeException) {
            return (RuntimeException) cause;
        }
        return new MustacheException(MustacheProblem.TEMPLATE_LOADING_ERROR,
                cause);
    }

    /**
     *
     * @author Martin Kouba
     */
    private static class DefaultMustacheCompilationEvent
            implements MustacheCompilationEvent {

        private final Mustache mustache;

        public DefaultMustacheCompilationEvent(Mustache mustache) {
            super();
            this.mustache = mustache;
        }

        @Override
        public Mustache getMustache() {
            return mustache;
        }

    }

    /**
     *
     * @author Martin Kouba
     */
    private static class DefaultMustacheParsingEvent
            implements MustacheParsingEvent {

        private final String mustacheName;

        private Reader reader;

        public DefaultMustacheParsingEvent(String mustacheName, Reader reader) {
            super();
            this.mustacheName = mustacheName;
            this.reader = reader;
        }

        public String getMustacheName() {
            return mustacheName;
        }

        @Override
        public Reader getMustacheContents() {
            return reader;
        }

        @Override
        public void setMustacheContents(Reader reader) {
            this.reader = reader;
        }

    }

}


/*
 * Copyright 2013 Martin Kouba
 *
 * Licensed 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.trimou.engine.parser;

import static org.trimou.engine.config.EngineConfigurationKey.REMOVE_STANDALONE_LINES;
import static org.trimou.engine.config.EngineConfigurationKey.REMOVE_UNNECESSARY_SEGMENTS;
import static org.trimou.engine.config.EngineConfigurationKey.REUSE_LINE_SEPARATOR_SEGMENTS;
import static org.trimou.exception.MustacheProblem.COMPILE_INVALID_TAG;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.regex.Matcher;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.Mustache;
import org.trimou.engine.MustacheEngine;
import org.trimou.engine.MustacheTagType;
import org.trimou.engine.config.EngineConfigurationKey;
import org.trimou.engine.segment.CommentSegment;
import org.trimou.engine.segment.ContainerSegment;
import org.trimou.engine.segment.ExtendSectionSegment;
import org.trimou.engine.segment.ExtendSegment;
import org.trimou.engine.segment.InvertedSectionSegment;
import org.trimou.engine.segment.LineSeparatorSegment;
import org.trimou.engine.segment.Origin;
import org.trimou.engine.segment.PartialSegment;
import org.trimou.engine.segment.RootSegment;
import org.trimou.engine.segment.SectionSegment;
import org.trimou.engine.segment.Segment;
import org.trimou.engine.segment.SegmentType;
import org.trimou.engine.segment.SetDelimitersSegment;
import org.trimou.engine.segment.TextSegment;
import org.trimou.engine.segment.ValueSegment;
import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;
import org.trimou.util.ImmutableList;
import org.trimou.util.ImmutableList.ImmutableListBuilder;
import org.trimou.util.Patterns;
import org.trimou.util.Strings;

/**
 * The default handler implementation that compiles the template. It's not
 * thread-safe and must not be reused.
 *
 * @author Martin Kouba
 */
class DefaultParsingHandler implements ParsingHandler {

    private static final Logger LOGGER = LoggerFactory
            .getLogger(DefaultParsingHandler.class);

    private final Deque<ContainerSegmentBase> containerStack = new ArrayDeque<>();

    private final List<Template> nestedTemplates = new ArrayList<>();

    private MustacheEngine engine;

    private String templateName;

    private Delimiters delimiters;

    private Template template;

    private long start;

    private int line = 1;

    private int index = 0;

    private boolean skipValueEscaping;

    private boolean handlebarsSupportEnabled;

    private NestedTemplateBase currentNestedBase;

    @Override
    public void startTemplate(String name, Delimiters delimiters,
            MustacheEngine engine) {

        this.delimiters = delimiters;
        this.engine = engine;
        this.templateName = name;

        containerStack.addFirst(new RootSegmentBase());

        skipValueEscaping = engine.getConfiguration().getBooleanPropertyValue(
                EngineConfigurationKey.SKIP_VALUE_ESCAPING);
        handlebarsSupportEnabled = engine.getConfiguration()
                .getBooleanPropertyValue(
                        EngineConfigurationKey.HANDLEBARS_SUPPORT_ENABLED);

        start = System.currentTimeMillis();
        LOGGER.debug("Start compilation of {}", new Object[] { name });
    }

    @Override
    public void endTemplate() {

        RootSegmentBase rootSegmentBase = validate();

        // Post processing
        if (engine.getConfiguration()
                .getBooleanPropertyValue(REMOVE_STANDALONE_LINES)) {
            SegmentBases.removeStandaloneLines(rootSegmentBase);
        }
        if (engine.getConfiguration()
                .getBooleanPropertyValue(REMOVE_UNNECESSARY_SEGMENTS)) {
            SegmentBases.removeUnnecessarySegments(rootSegmentBase);
        }
        if (engine.getConfiguration()
                .getBooleanPropertyValue(REUSE_LINE_SEPARATOR_SEGMENTS)) {
            SegmentBases.reuseLineSeparatorSegments(rootSegmentBase);
        }

        template = new Template(engine.getConfiguration()
                .getIdentifierGenerator().generate(Mustache.class),
                templateName, engine, nestedTemplates);
        template.initRootSegment(rootSegmentBase.asSegment(template));
        for (Template nested : nestedTemplates) {
            nested.initParent(template);
        }

        LOGGER.debug("Compilation of {} finished [time: {} ms, segments: {}]",
                templateName, System.currentTimeMillis() - start,
                template.getRootSegment().getSegmentsSize(true));

        nestedTemplates.clear();
        containerStack.clear();
    }

    @Override
    public void text(String text) {
        addSegment(new SegmentBase(SegmentType.TEXT, text, line,
                incrementAndGetIndex()));
    }

    @Override
    public void tag(ParsedTag tag) {

        validateTag(tag);

        switch (tag.getType()) {
        case COMMENT:
            addSegment(new SegmentBase(tag, line, incrementAndGetIndex()));
            break;
        case UNESCAPE_VARIABLE:
        case VARIABLE:
            valueSegment(tag);
            break;
        case PARTIAL:
            addSegment(
                    new PartialSegmentBase(tag, line, incrementAndGetIndex()));
            break;
        case DELIMITER:
            changeDelimiters(tag.getContent());
            addSegment(new SegmentBase(tag, line, incrementAndGetIndex()));
            break;
        case SECTION:
        case INVERTED_SECTION:
        case EXTEND:
        case EXTEND_SECTION:
            push(new ContainerSegmentBase(tag, line, incrementAndGetIndex()));
            break;
        case NESTED_TEMPLATE:
            nestedTemplate(tag);
            break;
        case SECTION_END:
            endSection(tag.getContent());
            break;
        default:
            throw new IllegalStateException("Unsupported tag type");
        }
    }

    @Override
    public void lineSeparator(String separator) {
        addSegment(
                new LineSeparatorBase(separator, line, incrementAndGetIndex()));
        line++;
    }

    public Mustache getCompiledTemplate() {
        if (template == null) {
            throw new MustacheException(MustacheProblem.TEMPLATE_NOT_READY);
        }
        return template;
    }

    private void validateTag(ParsedTag tag) {

        if (Strings.isEmpty(tag.getContent())) {
            throw new MustacheException(COMPILE_INVALID_TAG,
                    "Tag has no content [type: %s, line: %s]", tag.getType(),
                    line);
        }

        if (tag.getContent().contains(delimiters.getStart())) {
            throw new MustacheException(COMPILE_INVALID_TAG,
                    "Tag content contains current start delimiter [type: %s, line: %s, delimiter: %s]",
                    tag.getType(), line, delimiters.getStart());
        }

        if (handlebarsSupportEnabled) {
            // Only validate segments which cannot have a HelperExecutionHandler
            // associated
            if (MustacheTagType.contentMustBeValidated(tag.getType())
                    && !MustacheTagType.supportsHelpers(tag.getType())
                    && Strings.containsWhitespace(tag.getContent())) {
                contentMustBeNonWhitespaceSequenceException(tag.getType());
            }
        } else {
            if (MustacheTagType
                    .contentMustBeNonWhitespaceCharacterSequence(tag.getType())
                    && Strings.containsWhitespace(tag.getContent())) {
                contentMustBeNonWhitespaceSequenceException(tag.getType());
            }
        }
    }

    private MustacheException contentMustBeNonWhitespaceSequenceException(
            MustacheTagType tagType) {
        throw new MustacheException(COMPILE_INVALID_TAG,
                "Tag content must be a non-whitespace character sequence [template: %s, type: %s, line: %s]",
                templateName, tagType, line);
    }

    private void endSection(String key) {

        ContainerSegmentBase container = pop();

        if (container == null || SegmentType.ROOT.equals(container.getType())
                || (!handlebarsSupportEnabled
                        && !key.equals(container.getContent()))
                || (handlebarsSupportEnabled
                        && !container.getContent().startsWith(key))
                || (handlebarsSupportEnabled
                        && !container.getContent().contains(" ")
                        && !key.equals(container.getContent()))) {
            // a) No container on the stack
            // b) Handlebars support not enabled and section start key does not
            // equal to section end key
            // c) Handlebars support enabled and section start key does not
            // start with section end key

            StringBuilder msg = new StringBuilder();
            List<String> params = new ArrayList<>();
            msg.append("Invalid section end: ");
            if (container == null
                    || SegmentType.ROOT.equals(container.getType())) {
                msg.append("%s has no matching section start");
                params.add(key);
            } else {
                msg.append("%s is not matching section start %s");
                params.add(key);
                params.add(container.getContent());
            }
            msg.append(" [line: %s]");
            params.add("" + line);
            throw new MustacheException(
                    MustacheProblem.COMPILE_INVALID_SECTION_END, msg.toString(),
                    params.toArray());
        }

        if (container instanceof NestedTemplateBase) {
            // Do not add nested template as a segment
            NestedTemplateBase nestedBase = (NestedTemplateBase) container;
            Template nested = new Template(engine.getConfiguration()
                    .getIdentifierGenerator().generate(Mustache.class),
                    container.getContent(), engine);
            nested.initRootSegment(nestedBase.asSegment(nested));
            nestedTemplates.add(nested);
            currentNestedBase = null;
        } else {
            addSegment(container);
        }
    }

    /**
     * E.g. =<% %>=, =[ ]=
     *
     * @param key
     */
    private void changeDelimiters(String key) {

        if (key.charAt(0) != MustacheTagType.DELIMITER.getCommand()
                || key.charAt(key.length() - 1) != MustacheTagType.DELIMITER
                        .getCommand()) {
            throw new MustacheException(
                    MustacheProblem.COMPILE_INVALID_DELIMITERS,
                    "Invalid set delimiters tag: %s [line: %s]", key, line);
        }

        Matcher matcher = Patterns.newSetDelimitersContentPattern()
                .matcher(key.substring(1, key.length() - 1));

        if (matcher.find()) {
            delimiters.setNewValues(matcher.group(1), matcher.group(3));
        } else {
            throw new MustacheException(
                    MustacheProblem.COMPILE_INVALID_DELIMITERS,
                    "Invalid delimiters set: %s [line: %s]", key, line);
        }
    }

    /**
     * Push the container wrapper on the stack.
     *
     * @param container
     */
    private void push(ContainerSegmentBase container) {
        containerStack.addFirst(container);
        LOGGER.trace("Push {} [name: {}]", container.getType(),
                container.getContent());
    }

    /**
     *
     * @return the container wrapper removed from the stack
     */
    private ContainerSegmentBase pop() {
        ContainerSegmentBase container = containerStack.removeFirst();
        LOGGER.trace("Pop {} [name: {}]", container.getType(),
                container.getContent());
        return container;
    }

    /**
     * Add the segment to the container on the stack.
     *
     * @param segment
     */
    private void addSegment(SegmentBase segment) {
        containerStack.peekFirst().addSegment(segment);
        LOGGER.trace("Add {}", segment);
    }

    /**
     * Validate the compiled template.
     */
    private RootSegmentBase validate() {

        ContainerSegmentBase root = containerStack.peekFirst();

        if (!(root instanceof RootSegmentBase)) {
            throw new MustacheException(
                    MustacheProblem.COMPILE_INVALID_TEMPLATE,
                    "Incorrect last container segment on the stack: %s",
                    containerStack.peekFirst().toString(), line);
        }
        return (RootSegmentBase) root;
    }

    private int incrementAndGetIndex() {
        return ++index;
    }

    private void valueSegment(ParsedTag tag) {
        addSegment(new ValueSegmentBase(tag, line, incrementAndGetIndex(),
                skipValueEscaping));
    }

    private void nestedTemplate(ParsedTag tag) {
        if (engine.getConfiguration().getBooleanPropertyValue(
                EngineConfigurationKey.NESTED_TEMPLATE_SUPPORT_ENABLED)) {
            // First check existing nested templates
            for (Template nested : nestedTemplates) {
                if (nested.getName().equals(tag.getContent())) {
                    throw new MustacheException(
                            MustacheProblem.COMPILE_NESTED_TEMPLATE_ERROR,
                            "A nested template with the name [%s] is already defined at line %s in the template [%s]",
                            tag.getContent(),
                            // The root segment of a nested template references
                            // the line from the defining template
                            nested.getRootSegment().getOrigin().getLine(),
                            templateName);
                }
            }
            NestedTemplateBase nestedBase = new NestedTemplateBase(
                    tag.getContent(), line, index);
            if (currentNestedBase != null) {
                throw new MustacheException(
                        MustacheProblem.COMPILE_NESTED_TEMPLATE_ERROR,
                        "Nested templates within nested template definitions are not supported: %s",
                        containerStack.peekFirst().toString());
            } else {
                currentNestedBase = nestedBase;
            }
            push(nestedBase);
        } else {
            valueSegment(
                    new ParsedTag(
                            MustacheTagType.NESTED_TEMPLATE.getCommand()
                                    + tag.getContent(),
                            MustacheTagType.VARIABLE));
        }
    }

    /**
     * Root segment
     */
    static class RootSegmentBase extends ContainerSegmentBase {

        RootSegmentBase() {
            super(SegmentType.ROOT, null, 0, 0);
        }

        @Override
        public RootSegment asSegment(Template template) {
            return new RootSegment(new Origin(template), getSegments(template));
        }

    }

    static class NestedTemplateBase extends ContainerSegmentBase {

        NestedTemplateBase(String content, int line, int index) {
            super(null, content, line, index);
        }

        @Override
        public RootSegment asSegment(Template template) {
            return new RootSegment(new Origin(template, getLine(), getIndex()),
                    getSegments(template));
        }

    }

    static class ContainerSegmentBase extends SegmentBase
            implements Iterable<SegmentBase> {

        private final List<SegmentBase> segments;

        ContainerSegmentBase(SegmentType type, String content, int line,
                int index) {
            super(type, content, line, index);
            this.segments = new ArrayList<>();
        }

        ContainerSegmentBase(ParsedTag tag, int line, int index) {
            super(tag, line, index);
            this.segments = new ArrayList<>();
        }

        boolean addSegment(SegmentBase segment) {
            if (SegmentType.EXTEND.equals(getType())
                    && !SegmentType.EXTEND_SECTION.equals(segment.getType())) {
                // Only add extending sections
                return false;
            }
            return segments.add(segment);
        }

        @Override
        ContainerSegment asSegment(Template template) {
            switch (getType()) {
            case SECTION:
                return new SectionSegment(getContent(), getOrigin(template),
                        getSegments(template));
            case INVERTED_SECTION:
                return new InvertedSectionSegment(getContent(),
                        getOrigin(template), getSegments(template));
            case EXTEND:
                return new ExtendSegment(getContent(), getOrigin(template),
                        getSegments(template));
            case EXTEND_SECTION:
                return new ExtendSectionSegment(getContent(),
                        getOrigin(template), getSegments(template));
            default:
                throw new IllegalStateException(
                        "Invalid tag type: " + getType());
            }
        }

        protected List<Segment> getSegments(Template template) {
            ImmutableListBuilder<Segment> builder = ImmutableList.builder();
            for (SegmentBase wrapper : segments) {
                builder.add(wrapper.asSegment(template));
            }
            return builder.build();
        }

        @Override
        public Iterator<SegmentBase> iterator() {
            return segments.iterator();
        }

        ListIterator<SegmentBase> listIterator() {
            return segments.listIterator();
        }

    }

    static class LineSeparatorBase extends SegmentBase {

        // Cache the segment so that reuse is possible
        private LineSeparatorSegment segment;

        public LineSeparatorBase(String content, int line, int index) {
            super(SegmentType.LINE_SEPARATOR, content, line, index);
        }

        @Override
        Segment asSegment(Template template) {
            if (segment == null) {
                segment = new LineSeparatorSegment(getContent(),
                        getOrigin(template));
            }
            return segment;
        }

    }

    static class ValueSegmentBase extends SegmentBase {

        private boolean unescape;

        ValueSegmentBase(ParsedTag tag, int line, int index, boolean skipValueEscaping) {
            super(SegmentType.VALUE, tag.getContent(), line, index);
            unescape = skipValueEscaping || tag.getType().equals(MustacheTagType.UNESCAPE_VARIABLE);
        }

        @Override
        ValueSegment asSegment(Template template) {
            return new ValueSegment(getContent(), getOrigin(template),
                    unescape);
        }

    }

    static class PartialSegmentBase extends SegmentBase {

        private String indentation;

        PartialSegmentBase(ParsedTag tag, int line, int index) {
            super(tag, line, index);
        }

        public void setIndentation(String indentation) {
            this.indentation = indentation;
        }

        @Override
        public PartialSegment asSegment(Template template) {
            return new PartialSegment(getContent(), getOrigin(template),
                    indentation);
        }

    }

    static class SegmentBase {

        private final SegmentType type;

        private final String content;

        private final int line;

        private final int index;

        SegmentBase(ParsedTag tag, int line, int index) {
            this.content = tag.getContent();
            this.type = SegmentType.fromTag(tag.getType());
            this.line = line;
            this.index = index;
        }

        SegmentBase(SegmentType type, String content, int line, int index) {
            this.type = type;
            this.content = content;
            this.line = line;
            this.index = index;
        }

        SegmentType getType() {
            return type;
        }

        String getContent() {
            return content;
        }

        int getLine() {
            return line;
        }

        int getIndex() {
            return index;
        }

        Segment asSegment(Template template) {
            switch (type) {
            case TEXT:
                return new TextSegment(content, getOrigin(template));
            case COMMENT:
                return new CommentSegment(content, getOrigin(template));
            case DELIMITERS:
                return new SetDelimitersSegment(content, getOrigin(template));
            default:
                throw new IllegalStateException(
                        "Unsupported segment type: " + type);
            }
        }

        protected Origin getOrigin(Template template) {
            return new Origin(template, line, index);
        }

        @Override
        public String toString() {
            return String.format("[type: %s, content: %s, line: %s, idx: %s]",
                    type, content, line, index);
        }

    }

}