/**
 * Copyright 2011-2014 the original author or authors.
 */
package com.jetdrone.vertx.yoke.engine;

import com.jetdrone.vertx.yoke.core.YokeAsyncResult;
import org.vertx.java.core.AsyncResult;
import org.vertx.java.core.AsyncResultHandler;
import org.vertx.java.core.Handler;
import org.vertx.java.core.buffer.Buffer;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * # StringPlaceholderEngine
 */
public class StringPlaceholderEngine extends AbstractEngine<String> {

    private static final String placeholderPrefix = "${";
    private static final String placeholderSuffix = "}";
    private static final boolean ignoreUnresolvablePlaceholders = true;

    private static final String funcName = "([a-zA-Z0-9]+)";
    private static final String arguments = "\\((.*)\\)";
    private static final Pattern FUNCTION = Pattern.compile(funcName + "\\s*" + arguments);

    private static final String argument = "(.*?)";
    private static final String quote = "\'";
    private static final String sep = "(,\\s*)?";
    private static final Pattern ARG = Pattern.compile(quote + argument + quote + sep);

    /**
     * An interpreter for strings with named placeholders.
     *
     * For example given the string "hello ${myName}" and the map <code>
     *      <p>Map<String, Object> map = new HashMap<String, Object>();</p>
     *      <p>map.put("myName", "world");</p>
     * </code>
     *
     * the call returns "hello world"
     *
     * It replaces every occurrence of a named placeholder with its given value
     * in the map. If there is a named place holder which is not found in the
     * map then the string will retain that placeholder. Likewise, if there is
     * an entry in the map that does not have its respective placeholder, it is
     * ignored.
     */
    @Override
    public void render(final String file, final Map<String, Object> context, final Handler<AsyncResult<Buffer>> handler) {
        // verify if the file is still fresh in the cache
        read(file, new AsyncResultHandler<String>() {
            @Override
            public void handle(AsyncResult<String> asyncResult) {
                if (asyncResult.failed()) {
                    handler.handle(new YokeAsyncResult<Buffer>(asyncResult.cause()));
                } else {
                    try {
                        handler.handle(new YokeAsyncResult<>(parseStringValue(asyncResult.result(), context, new HashSet<String>())));
                    } catch (IllegalArgumentException iae) {
                        handler.handle(new YokeAsyncResult<Buffer>(iae));
                    }
                }
            }
        });
    }

    private Buffer parseStringValue(String template, Map<String, Object> context, Set<String> visitedPlaceholders) {
        StringBuilder buf = new StringBuilder(template);

        int startIndex = template.indexOf(placeholderPrefix);
        while (startIndex != -1) {
            int endIndex = findPlaceholderEndIndex(buf, startIndex);
            if (endIndex != -1) {
                String placeholder = buf.substring(startIndex + placeholderPrefix.length(), endIndex);
                if (!visitedPlaceholders.add(placeholder)) {
                    throw new IllegalArgumentException(
                            "Circular placeholder reference '" + placeholder + "' in property definitions");
                }
                // Recursive invocation, parsing placeholders contained in the placeholder key.
                placeholder = parseStringValue(placeholder, context, visitedPlaceholders).toString(contentEncoding());

                // Now obtain the value for the fully resolved key...
                Object propVal;
                boolean isFn = false;

                Matcher fn = FUNCTION.matcher(placeholder);
                if (fn.find()) {
                    // function syntax used, get the object from context with the proper name
                    propVal = context.get(fn.group(1));
                    isFn = true;
                } else {
                    propVal = context.get(placeholder);
                }

                if (propVal != null) {
                    // Recursive invocation, parsing placeholders contained in the
                    // previously resolved placeholder value.
                    String propValStr;
                    if (isFn && propVal instanceof Function) {
                        Matcher arg = ARG.matcher(fn.group(2));
                        List<Object> args = null;

                        while (arg.find()) {
                            if (args == null) {
                                args = new ArrayList<>();
                            }
                            args.add(arg.group(1));
                        }
                        if (args == null) {
                            propValStr = ((Function) propVal).exec(context);
                        } else {
                            propValStr = ((Function) propVal).exec(context, args.toArray());
                        }
                    } else {
                        propValStr = propVal.toString();
                    }
                    propValStr = parseStringValue(propValStr, context, visitedPlaceholders).toString(contentEncoding());
                    buf.replace(startIndex, endIndex + placeholderSuffix.length(), propValStr);

                    startIndex = buf.indexOf(placeholderPrefix, startIndex + propValStr.length());
                }
                else if (ignoreUnresolvablePlaceholders) {
                    // Proceed with unprocessed value.
                    startIndex = buf.indexOf(placeholderPrefix, endIndex + placeholderSuffix.length());
                }
                else {
                    throw new IllegalArgumentException("Could not resolve placeholder '" + placeholder + "'");
                }

                visitedPlaceholders.remove(placeholder);
            }
            else {
                startIndex = -1;
            }
        }

        return new Buffer(buf.toString());
    }

    private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
        int index = startIndex + placeholderPrefix.length();
        int withinNestedPlaceholder = 0;
        while (index < buf.length()) {
            if (substringMatch(buf, index, placeholderSuffix)) {
                if (withinNestedPlaceholder > 0) {
                    withinNestedPlaceholder--;
                    index = index + placeholderPrefix.length() - 1;
                }
                else {
                    return index;
                }
            }
            else if (substringMatch(buf, index, placeholderPrefix)) {
                withinNestedPlaceholder++;
                index = index + placeholderPrefix.length();
            }
            else {
                index++;
            }
        }
        return -1;
    }

    private static boolean substringMatch(CharSequence str, int index, CharSequence substring) {
        for (int j = 0; j < substring.length(); j++) {
            int i = index + j;
            if (i >= str.length() || str.charAt(i) != substring.charAt(j)) {
                return false;
            }
        }
        return true;
    }
}

@@ -2675,32 +2675,1094 @@
        %7D);%0A    %7D
+%0A    %0A    @Override%0A    public void render(final String file, final String layoutFile, final Map%3CString, Object%3E context, final Handler%3CAsyncResult%3CBuffer%3E%3E handler) %7B%0A        %0A        /* For now the focus was to implement the layout support in the Groovy template engine */%0A        /* This will follow as soon as possible */%0A        %0A        // verify if the file is still fresh in the cache%0A        read(file, new AsyncResultHandler%3CString%3E() %7B%0A            @Override%0A            public void handle(AsyncResult%3CString%3E asyncResult) %7B%0A                if (asyncResult.failed()) %7B%0A                    handler.handle(new YokeAsyncResult%3CBuffer%3E(asyncResult.cause()));%0A                %7D else %7B%0A                    try %7B%0A                        handler.handle(new YokeAsyncResult%3C%3E(parseStringValue(asyncResult.result(), context, new HashSet%3CString%3E())));%0A                    %7D catch (IllegalArgumentException iae) %7B%0A                        handler.handle(new YokeAsyncResult%3CBuffer%3E(iae));%0A                    %7D%0A                %7D%0A            %7D%0A        %7D);%0A    %7D     
 %0A%0A    private Bu

Update StringPlaceholderEngine.java