/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

/*
 * Created on Jul 25, 2003
 */
package org.apache.jmeter.engine.util;

import java.io.IOException;
import java.io.StringReader;
import java.util.LinkedList;

import org.apache.jmeter.engine.StandardJMeterEngine;
import org.apache.jmeter.functions.Function;
import org.apache.jmeter.functions.InvalidVariableException;
import org.apache.jmeter.testelement.TestListener;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;

/**
 * Parses function / variable references of the form 
 * ${functionName[([var[,var...]])]} 
 * and 
 * ${variableName}
 */
class FunctionParser {
	private static final Logger log = LoggingManager.getLoggerForClass();

	/**
	 * Compile a general string into a list of elements for a CompoundVariable.
	 * 
	 * Calls {@link #makeFunction(StringReader)} if it detects an unescaped "${".
	 * 
	 * @param value string containing the function / variable references (if any)
	 * 
	 * @return list of strings or Objects representing functions
	 */
	LinkedList compileString(String value) throws InvalidVariableException {
		StringReader reader = new StringReader(value);
		LinkedList result = new LinkedList();
		StringBuffer buffer = new StringBuffer();
		char previous = ' '; // TODO - why use space?
		char[] current = new char[1];
		try {
			while (reader.read(current) == 1) {
				if (current[0] == '\\') {
					previous = current[0];
					if (reader.read(current) == 0) {
						break;
					}
					if (current[0] != '$' && current[0] != ',' && current[0] != '\\') {
						buffer.append(previous); // i.e. '\\'
					}
					previous = ' ';
					buffer.append(current[0]);
					continue;
				} else if (current[0] == '{' && previous == '$') {// found "${"
					buffer.deleteCharAt(buffer.length() - 1);
					if (buffer.length() > 0) {// save leading text
						result.add(buffer.toString());
						buffer.setLength(0);
					}
					result.add(makeFunction(reader));
					previous = ' ';
				} else {
					buffer.append(current[0]);
					previous = current[0];
				}
			}
			if (buffer.length() > 0) {
				result.add(buffer.toString());
			}
		} catch (IOException e) {
			log.error("Error parsing function: " + value, e);
			result.clear();
			result.add(value);
		}
		if (result.size() == 0) {
			result.add("");
		}
		return result;
	}

	/**
	 * Compile a string into a function or SimpleVariable.
	 * 
	 * Called by {@link #compileString(String)} when that has detected "${".
	 * 
	 * Calls {@link CompoundVariable#getNamedFunction(String)} if it detects: 
	 * '(' - start of parameter list
	 * '}' - end of function call
	 *  
	 * @param reader points to input after the "${"
	 * @return the function or variable object (or a String)
	 */
	Object makeFunction(StringReader reader) throws InvalidVariableException {
		char[] current = new char[1];
		char previous = ' '; // TODO - why use space?
		StringBuffer buffer = new StringBuffer();
		Object function;
		try {
			while (reader.read(current) == 1) {
				if (current[0] == '\\') {
					if (reader.read(current) == 0) {
						break;
					}
					previous = ' ';
					buffer.append(current[0]);
					continue;
				} else if (current[0] == '(' && previous != ' ') {
                    String funcName = buffer.toString();
					function = CompoundVariable.getNamedFunction(funcName);
					buffer.setLength(0);
					if (function instanceof Function) {
						((Function) function).setParameters(parseParams(reader));
						if (reader.read(current) == 0 || current[0] != '}') {
                            reader.reset();// set to start of string
                            char []cb = new char[100];
                            reader.read(cb);// return deliberately ignored
							throw new InvalidVariableException
                            ("Expected } after "+funcName+" function call in "+new String(cb));
						}
						if (function instanceof TestListener) {
							StandardJMeterEngine.register((TestListener) function);
						}
						return function;
					}
					continue;
				} else if (current[0] == '}') {// variable, or function with no parameter list
					function = CompoundVariable.getNamedFunction(buffer.toString());
					if (function instanceof Function){// ensure that setParameters() is called.
						((Function) function).setParameters(new LinkedList());
					}
					buffer.setLength(0);
					return function;
				} else {
					buffer.append(current[0]);
					previous = current[0];
				}
			}
		} catch (IOException e) {
			log.error("Error parsing function: " + buffer.toString(), e);
			return null;
		}
		log.warn("Probably an invalid function string: " + buffer.toString());
		return buffer.toString();
	}

	/**
	 * Compile a String into a list of parameters, each made into a
	 * CompoundVariable.
	 */
	LinkedList parseParams(StringReader reader) throws InvalidVariableException {
		LinkedList result = new LinkedList();
		StringBuffer buffer = new StringBuffer();
		char[] current = new char[1];
		char previous = ' ';
		int functionRecursion = 0;
		int parenRecursion = 0;
		try {
			while (reader.read(current) == 1) {
				if (current[0] == '\\') {
					buffer.append(current[0]);
					if (reader.read(current) == 0) {
						break;
					}
					previous = ' ';
					buffer.append(current[0]);
					continue;
				} else if (current[0] == ',' && functionRecursion == 0) {
					CompoundVariable param = new CompoundVariable();
					param.setParameters(buffer.toString());
					buffer.setLength(0);
					result.add(param);
				} else if (current[0] == ')' && functionRecursion == 0 && parenRecursion == 0) {
					// Detect functionName() so this does not generate empty string as the parameter
					if (buffer.length() == 0 && result.isEmpty()){
						return result;
					}
					CompoundVariable param = new CompoundVariable();
					param.setParameters(buffer.toString());
					buffer.setLength(0);
					result.add(param);
					return result;
				} else if (current[0] == '{' && previous == '$') {
					buffer.append(current[0]);
					previous = current[0];
					functionRecursion++;
				} else if (current[0] == '}' && functionRecursion > 0) {
					buffer.append(current[0]);
					previous = current[0];
					functionRecursion--;
				} else if (current[0] == ')' && functionRecursion == 0 && parenRecursion > 0) {
					buffer.append(current[0]);
					previous = current[0];
					parenRecursion--;
				} else if (current[0] == '(' && functionRecursion == 0) {
					buffer.append(current[0]);
					previous = current[0];
					parenRecursion++;
				} else {
					buffer.append(current[0]);
					previous = current[0];
				}
			}
		} catch (IOException e) {
			log.error("Error parsing function: " + buffer.toString(), e);
		}
		log.warn("Probably an invalid function string: " + buffer.toString());
		CompoundVariable var = new CompoundVariable();
		var.setParameters(buffer.toString());
		result.add(var);
		return result;
	}
}
