/*
 * Copyright 2004-2016 the original author or authors.
 * 
 * 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
 * 
 *      https://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.binding.expression.support;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.springframework.binding.expression.Expression;
import org.springframework.binding.expression.ExpressionParser;
import org.springframework.binding.expression.ExpressionVariable;
import org.springframework.binding.expression.ParserContext;
import org.springframework.binding.expression.ParserException;
import org.springframework.util.Assert;

/**
 * Abstract base class for parsing ${...} style expressions.
 * 
 * @author Keith Donald
 * @see org.springframework.binding.expression.beanwrapper.BeanWrapperExpressionParser
 */
public abstract class AbstractExpressionParser implements ExpressionParser {

	/**
	 * The expression prefix.
	 */
	private static final String DEFAULT_EXPRESSION_PREFIX = "${";

	/**
	 * The expression suffix.
	 */
	private static final String DEFAULT_EXPRESSION_SUFFIX = "}";

	/**
	 * The marked expression delimiter prefix.
	 */
	private String expressionPrefix = DEFAULT_EXPRESSION_PREFIX;

	/**
	 * The marked expression delimiter suffix.
	 */
	private String expressionSuffix = DEFAULT_EXPRESSION_SUFFIX;

	/**
	 * Should we allow delimited eval expressions like "${foo.bar}"? If not, evalutable expressions must not be enclosed
	 * in delimiters like ${foo.bar} else an exception is thrown. Only here for compatability reasons, as Web Flow 1.0
	 * allows delimited eval expressions while 2.x does not.
	 */
	private boolean allowDelimitedEvalExpressions;

	/**
	 * Returns the configured expression delimiter prefix. Defaults to "${".
	 */
	public String getExpressionPrefix() {
		return expressionPrefix;
	}

	/**
	 * Sets the expression delimiter prefix.
	 */
	public void setExpressionPrefix(String expressionPrefix) {
		this.expressionPrefix = expressionPrefix;
	}

	/**
	 * Returns the expression delimiter suffix. Defaults to "}".
	 */
	public String getExpressionSuffix() {
		return expressionSuffix;
	}

	/**
	 * Sets the expression delimiter suffix.
	 */
	public void setExpressionSuffix(String expressionSuffix) {
		this.expressionSuffix = expressionSuffix;
	}

	/**
	 * Returns if this parser allows delimited eval expressions like <code>${foo.bar}</code>.
	 */
	public boolean getAllowDelimitedEvalExpressions() {
		return allowDelimitedEvalExpressions;
	}

	/**
	 * Sets if this parser allows eval expressions like ${foo.bar}.
	 */
	public void setAllowDelimitedEvalExpressions(boolean allowDelmitedEvalExpressions) {
		this.allowDelimitedEvalExpressions = allowDelmitedEvalExpressions;
	}

	// expression parser

	public Expression parseExpression(String expressionString, ParserContext context) throws ParserException {
		Assert.notNull(expressionString, "The expression string to parse is required");
		if (context == null) {
			context = NullParserContext.INSTANCE;
		}
		if (context.isTemplate()) {
			return parseTemplate(expressionString, context);
		} else {
			if (expressionString.startsWith(getExpressionPrefix()) && expressionString.endsWith(getExpressionSuffix())) {
				if (!allowDelimitedEvalExpressions) {
					throw new ParserException(
							expressionString,
							"The expression '" + expressionString + "' being parsed is expected be an expression. " +
									"Do not enclose such expression strings in ${} delimiters as it's redundant. " +
									"If you need to parse a template that mixes literal text with evaluatable blocks, " +
									"set the 'template' parser context attribute to true.",
							null);
				} else {
					int lastIndex = expressionString.length() - getExpressionSuffix().length();
					String expression = expressionString.substring(getExpressionPrefix().length(), lastIndex);
					return doParseExpression(expression, context);
				}
			} else {
				return doParseExpression(expressionString, context);
			}
		}
	}

	private Expression parseTemplate(String expressionString, ParserContext context) throws ParserException {
		Assert.notNull(expressionString, "The expression string to parse is required");
		if (expressionString.length() == 0) {
			return parseEmptyExpressionString(context);
		}
		Expression[] expressions = parseExpressions(expressionString, context);
		if (expressions.length == 1) {
			return expressions[0];
		} else {
			return new CompositeStringExpression(expressions);
		}
	}

	// helper methods

	/**
	 * Helper that handles a empty expression string.
	 */
	private Expression parseEmptyExpressionString(ParserContext context) {
		if (allowDelimitedEvalExpressions) {
			// let the parser handle it
			return doParseExpression("", context);
		} else {
			// return a literal expression containing the empty string
			return new LiteralExpression("");
		}
	}

	/**
	 * Helper that parses given expression string using the configured parser. The expression string can contain any
	 * number of expressions all contained in "${...}" markers. For instance: "foo${expr0}bar${expr1}". The static
	 * pieces of text will also be returned as Expressions that just return that static piece of text. As a result,
	 * evaluating all returned expressions and concatenating the results produces the complete evaluated string.
	 * @param expressionString the expression string
	 * @return the parsed expressions
	 * @throws ParserException when the expressions cannot be parsed
	 */
	private Expression[] parseExpressions(String expressionString, ParserContext context) throws ParserException {
		List<Expression> expressions = new LinkedList<>();
		int startIdx = 0;
		while (startIdx < expressionString.length()) {
			int prefixIndex = expressionString.indexOf(getExpressionPrefix(), startIdx);
			if (prefixIndex >= startIdx) {
				// a inner expression was found - this is a composite
				if (prefixIndex > startIdx) {
					expressions.add(new LiteralExpression(expressionString.substring(startIdx, prefixIndex)));
					startIdx = prefixIndex;
				}
				int nextPrefixIndex = expressionString.indexOf(getExpressionPrefix(), prefixIndex
						+ getExpressionPrefix().length());
				int suffixIndex;
				if (nextPrefixIndex == -1) {
					// this is the last expression in the expression string
					suffixIndex = expressionString.lastIndexOf(getExpressionSuffix());
				} else {
					// another expression exists after this one in the expression string
					suffixIndex = expressionString.lastIndexOf(getExpressionSuffix(), nextPrefixIndex);
				}
				if (suffixIndex < (prefixIndex + getExpressionPrefix().length())) {
					throw new ParserException(expressionString, "No ending suffix '" + getExpressionSuffix()
							+ "' for expression starting at character " + prefixIndex + ": "
							+ expressionString.substring(prefixIndex), null);
				} else if (suffixIndex == prefixIndex + getExpressionPrefix().length()) {
					throw new ParserException(expressionString, "No expression defined within delimiter '"
							+ getExpressionPrefix() + getExpressionSuffix() + "' at character " + prefixIndex, null);
				} else {
					String expr = expressionString.substring(prefixIndex + getExpressionPrefix().length(), suffixIndex);
					expressions.add(doParseExpression(expr, context));
					startIdx = suffixIndex + 1;
				}
			} else {
				if (startIdx == 0) {
					// treat the entire string as one expression
					if (allowDelimitedEvalExpressions) {
						expressions.add(doParseExpression(expressionString, context));
					} else {
						// treat entire string as a literal
						expressions.add(new LiteralExpression(expressionString));
					}
				} else {
					// no more ${expressions} found in string, add rest as static text
					expressions.add(new LiteralExpression(expressionString.substring(startIdx)));
				}
				startIdx = expressionString.length();
			}
		}
		return expressions.toArray(new Expression[expressions.size()]);
	}

	protected Map<String, Expression> parseVariableExpressions(ExpressionVariable[] variables) throws ParserException {
		if (variables == null || variables.length == 0) {
			return null;
		}
		Map<String, Expression> variableExpressions = new HashMap<>(variables.length, 1);
		for (ExpressionVariable var : variables) {
			variableExpressions.put(var.getName(), parseExpression(var.getValueExpression(), var.getParserContext()));
		}
		return variableExpressions;
	}

	protected abstract Expression doParseExpression(String expressionString, ParserContext context)
			throws ParserException;

}
