/* Copyright Dassault Systemes, 1999, 2009 */


package examples.development.plugins.calculation;

import java.util.ArrayList;
import java.util.Collection;
import java.util.StringTokenizer;

import com.engineous.common.i18n.IString;
import com.engineous.sdk.calc.AbstractFunction;
import com.engineous.sdk.calc.AbstractScalarFunction;
import com.engineous.sdk.calc.CalcException;
import com.engineous.sdk.calc.Calculation;
import com.engineous.sdk.calc.CalculationPlugin;
import com.engineous.sdk.calc.Function;
import com.engineous.sdk.calc.FunctionEnv;
import com.engineous.sdk.calc.Operator;
import com.engineous.sdk.calc.ScalarEnv;
import com.engineous.sdk.resmgr.ResMgr;
import com.engineous.sdk.vars.ArrayVariable;
import com.engineous.sdk.vars.EsiTypes;
import com.engineous.sdk.vars.ScalarVariable;
import com.engineous.sdk.vars.Variable;
import com.engineous.sdk.vars.VariableException;

/**
 * A plug-in to the Calculator that adds operators that operate on Strings.
 * This is more of an example than a real feature.
 * <p>
 * This plug-in defines the following functions:
 * <ul>
 *   <li>concat(str, str....) - concatenate the strings returning one long string.
 *   <li>strlen(str) - return the number of characters in the string
 *   <li>index(str1, str2) - return the index in str1 of the first instance of str2
 *   <li>upcase(str) - return str converted to upper case
 *   <li>downcase(str) - return str converted to lower case
 *   <li>substr(str, start, end) - extracts a substring from str using the rules for java.lang.String.substring
 *   <li>join(arr, delim) - join all elements of into one string, separated by the delimiter string
 *   <li>split(string, delim) - split the string on the delimiters and return an array of strings
 * </ul>
 * It also defines one operator:
 * <ul>
 *   <li><tty><b>&</b></tty> - Infix string concatenation operator.
 * </ul>
 * <p>
 * Note that the Function Classes that implement the individual functions (and operator '&') are static internal
 * classes of class StringFunc.  This is not necessary, but keeps all the source in one neat package.
 */
public class StringFunc
		implements CalculationPlugin {

	transient private static final Class CLASS = StringFunc.class;

	/** Hold static instances of the Collections to avoid the need to re-build them every time. */
	private static Collection<Operator> operators;
	private static Collection<Function> functions;

	/**
	 * Get the functions defined in this package.
	 * @return Collection of Function objects.
	 * @see com.engineous.sdk.calc.CalculationPlugin#getFunctions(com.engineous.sdk.calc.CalculationPlugin.Env)
	 */
	public Collection<Function> getFunctions() {

		if (functions == null) {
			createFunctions();
		}

		return functions;
	}

	/**
	 * Returns the operators defined in this package.  Currently only '&'.
	 * @return Collection of Operator
	 * @see com.engineous.sdk.calc.CalculationPlugin#getOperators(com.engineous.sdk.calc.CalculationPlugin.Env)
	 */
	public Collection<Operator> getOperators() {

		if (operators == null) {
			createOperators();
		}
		return operators;
	}

	/**
	 * Create static list of functions.  Only called once.
	 */
	private void createFunctions() {

		functions = new ArrayList<Function>(8);
		functions.add(new Concat());
		functions.add(new StrLen());
		functions.add(new Index());
		functions.add(new UpDownCase(true));
		functions.add(new UpDownCase(false));
		functions.add(new Substr());
		functions.add(new Join());
		functions.add(new Split());
	}

	/**
	 * Create the static list of operators.  Only called once.
	 */
	private void createOperators() {

		// Get definition of '+' operator from Calculation engine so I can get its precedence.
		Calculation calc = Calculation.create(false);
		Operator add = calc.getOperator("+", Operator.OpType.INFIX);
		int concatPrecedence = add.getPrecedence() - 1;

		// Create concatenate operator backed by Concatenate function.
		// Precedence is just BELOW addition, so '"foo" & i + j is foo3, not '"foo1" is not a number'
		Operator concatOp = new Operator(Operator.OpType.INFIX, Operator.OpAssoc.LEFT, concatPrecedence, "&", new Concat(), null);

		operators = new ArrayList<Operator>(1);
		operators.add(concatOp);
	}


	/** Scalar function to concatenate 2 ore more Strings. */
	static class Concat
			extends AbstractScalarFunction {

		/** Construct function object, defining number of arguments and argument types */
		public Concat() {

			// Concat takes 2 or more scalar arguments.
			super("concat", -2);

			description = ResMgr.getMessage(CLASS, 61631, "Join 2 or more strings together");

			argType = EsiTypes.STRING;
			returnType = EsiTypes.STRING;
		}

		/**
		 * Evaluate a concatenation call - just stick the string arguments together in one long string.
		 * @param env
		 * @throws CalcException
		 * @throws VariableException
		 * @see com.engineous.sdk.calc.AbstractScalarFunction#eval(com.engineous.sdk.calc.ScalarEnv)
		 */
		@Override
		public void eval(ScalarEnv env)
				throws VariableException, CalcException {

			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < env.getNargs(); i++) {
				sb.append(env.getAsString(i));
			}
			env.setResult(sb.toString());
		}
	}


	/** Scalar function to take the length of one string */
	static class StrLen
			extends AbstractScalarFunction {

		/** Construct the Strlen function, supplying name, number of arguments, and argument types */
		public StrLen() {

			// Strlen takes exactly 1 scalar argument
			super("strlen", 1);

			description = ResMgr.getMessage(CLASS, 73224, "strlen(s) - Length of a string");

			argType = EsiTypes.STRING;
			returnType = EsiTypes.INTEGER;
		}

		/**
		 * Evaluate 'strlen' simply by calling Strign.length() on the String value of the argument.
		 * @param env
		 * @throws CalcException
		 * @throws VariableException
		 * @see com.engineous.sdk.calc.AbstractScalarFunction#eval(com.engineous.sdk.calc.ScalarEnv)
		 */
		@Override
		public void eval(ScalarEnv env)
				throws VariableException, CalcException {
			env.setResult(env.getAsString(0).length());
		}
	}


	/** Function to find one string inside another. */
	static class Index
			extends AbstractScalarFunction {

		/** Construct Index function, supplying name, number of arguments, and argument types. */
		public Index() {

			// Index takes exactly 2 scalar arguments
			super("index", 2);

			description = ResMgr.getMessage(CLASS, 44486, "index(s1, s2) - Search for string inside another string");

			argType = EsiTypes.STRING;
			returnType = EsiTypes.INTEGER;
		}

		/**
		 * Evaluate Index using String.indexOf(string)
		 * @param env
		 * @throws CalcException
		 * @throws VariableException
		 * @see com.engineous.sdk.calc.AbstractScalarFunction#eval(com.engineous.sdk.calc.ScalarEnv)
		 */
		@Override
		public void eval(ScalarEnv env)
				throws VariableException, CalcException {

			String s1 = env.getAsString(0);
			String s2 = env.getAsString(1);
			env.setResult(s1.indexOf(s2));
		}
	}


	/** Scalar function to convert a string to upper or lower case (depending on the constructor argument) */
	static class UpDownCase
			extends AbstractScalarFunction {

		// Flag to indicate if this instance is for upcase() or downcase()
		final boolean isUpcase;

		/**
		 * Construct the function.  The same code handles upcase and downcase, depending on argument to constructor
		 * @param isUpcase -
		 */
		public UpDownCase(boolean isUpcase) {

			// upcase and downcase take exactly 1 scalar argument
			super(isUpcase ? "upcase" : "downcase", 1);

			this.isUpcase = isUpcase;

			description = isUpcase
						  ? ResMgr.getMessage(CLASS, 46964, "upcase(s) - Convert string to upper case")
						  : ResMgr.getMessage(CLASS, 20686, "downcase(x) - Convert string to lower case");

			argType = EsiTypes.STRING;
			returnType = EsiTypes.STRING;
		}

		/**
		 * Evaluate using String.toUpperCase or String.toLowerCase
		 * @param env
		 * @throws CalcException
		 * @throws VariableException
		 * @see com.engineous.sdk.calc.AbstractScalarFunction#eval(com.engineous.sdk.calc.ScalarEnv)
		 */
		@Override
		public void eval(ScalarEnv env)
				throws VariableException, CalcException {

			String s = env.getAsString(0);
			if (isUpcase) {
				s = s.toUpperCase();
			}
			else {
				s = s.toLowerCase();
			}
			env.setResult(s);

		}
	}


	/** Function class to take a substring of an existing String using the java String.substring rules. */
	static class Substr
			extends AbstractScalarFunction {

		/** Construct Substr function.  Do NOT set argument types, as they are provided by getArgType below. */
		public Substr() {

			// substr takes exactly 3 scalar arguments
			super("substr", 3);

			description = ResMgr.getMessage(CLASS, 65982, "substr(s, start, end) - Substring, using java.lang.String rules");

			returnType = EsiTypes.STRING;
			// Arg types are handled below
		}

		/**
		 * Override AbstractFunction.getArgType to return String for the first argument and integer for the other 2.
		 * @param argNo
		 * @param nArgs
		 * @return Data type expected for Argument.
		 * @see com.engineous.sdk.calc.AbstractFunction#getArgType(int, int)
		 */
		@Override
		public String getArgType(int argNo, int nArgs) {

			if (argNo == 0) {
				return EsiTypes.STRING;
			}
			else {
				return EsiTypes.INTEGER;
			}
		}

		/**
		 * Evaluate Substr using String.substring(int start, int end).
		 * @param env
		 * @throws CalcException
		 * @throws VariableException
		 * @see com.engineous.sdk.calc.AbstractScalarFunction#eval(com.engineous.sdk.calc.ScalarEnv)
		 */
		@Override
		public void eval(ScalarEnv env)
				throws VariableException, CalcException {

			String s = env.getAsString(0);
			int start = (int)env.getAsInt(1);
			int end = (int)env.getAsInt(2);
			env.setResult(s.substring(start, end));
		}
	}


	/**
	 * Function to Join all elements of a String array into one String.
	 * This is NOT a ScalarFunction - the first argument must be an array.
	 */
	static class Join
			extends AbstractFunction {

		/** Constructor */
		public Join() {

			// Join takes exactly 2 arguments, a String Array and a String Scalar
			super("join", 2);

			description = ResMgr.getMessage(CLASS, 91406, "join(s_array, delim) - Join a string array, separating values with a string");

			// Return is always a string.  The arguments are a String array and a String scalar.
			returnStructure = Variable.STRUCT_SCALAR;
			returnType = EsiTypes.STRING;
			argType = EsiTypes.STRING;
		}

		/**
		 * Argument types.  First must be array, second must be scalar.
		 * @param argNo
		 * @param nArgs
		 * @return
		 * @see com.engineous.sdk.calc.AbstractFunction#getArgStructure(int, int)
		 */
		@Override
		public int getArgStructure(int argNo, int nArgs) {

			if (argNo == 0) {
				return Variable.STRUCT_ARRAY;
			}
			else {
				return Variable.STRUCT_SCALAR;
			}
		}

		/**
		 * Evalaute Join.  Loop over all elements of the array, building an output string.
		 * @param env
		 * @return Variable The resulting string as a ScalarVariable of type String
		 * @throws CalcException
		 * @throws VariableException
		 * @see com.engineous.sdk.calc.AbstractScalarFunction#eval(com.engineous.sdk.calc.ScalarEnv)
		 */
		@Override
		public Variable eval(FunctionEnv env)
				throws CalcException, VariableException {

			try {
				// Get the arguments as an Array and a Scalar.
				ArrayVariable arg1 = (ArrayVariable)env.getArgument(0);
				ScalarVariable separatorVar = (ScalarVariable)env.getArgument(1);

				// Get a Variable to hold the returned value.
				ScalarVariable resultVar = env.createScalar(EsiTypes.STRING);

				// Build the result string one array element at a time, inserting the separator between entries.
				String separator = separatorVar.getValueObj().getAsString();
				StringBuilder sb = new StringBuilder();
				for (int i = 0; i < arg1.getSize(); i++) {
					if (i > 0) {
						sb.append(separator);
					}
					sb.append(arg1.getValueObj(i).getAsString());
				}

				// Assign result string to the result Variable and return it.
				resultVar.getValueObj().setValue(sb.toString());
				return resultVar;
			}
			catch (ClassCastException ex) {
				throw new CalcException(ex, new IString(CLASS, 34379, "Expected argument Variable with different Structure: {0}", ex.getMessage()));
			}
		}
	}


	/**
	 * Function to split a string into an array of substrings.
	 * Note that this is not a ScalarFunction.  While the arguments must be scalar, the return value is an array of String.
	 *
	 * This uses StringTokenizer - strings are separated by arbitrary groups of characters drawn from the 'delim' argument.
	 */
	static class Split
			extends AbstractFunction {

		/**
		 * Construct the Split function, supplying all needed information.
		 */
		protected Split() {

			super("split", 2);

			description = ResMgr.getMessage(CLASS, 39225, "split(s, delim) - Split a String at delimiters");

			// Return is a string array
			returnStructure = Variable.STRUCT_ARRAY;
			returnType = EsiTypes.STRING;

			// Arguments are both Scalar Strings.
			argStructure = Variable.STRUCT_SCALAR;
			argType = EsiTypes.STRING;

		}

		/**
		 * Evaluate split.  This must take a FunctionEnv so it can return an ArrayVariable.
		 * @param env
		 * @return
		 * @throws CalcException
		 * @throws VariableException
		 * @see com.engineous.sdk.calc.AbstractFunction#eval(com.engineous.sdk.calc.FunctionEnv)
		 */
		@Override
		public Variable eval(FunctionEnv env)
				throws VariableException, CalcException {

			try {
				// Get the arguments.
				String s = ((ScalarVariable)env.getArgument(0)).getValueObj().getAsString();
				String delim = ((ScalarVariable)env.getArgument(1)).getValueObj().getAsString();

				// Construct the result as a resizable array of String.  Guess initial size at 10.
				// Result variable is used like an ArrayList - the size is doubled each time it overflows, and then trimmed to
				// the final size before returning it.  This relies on setDimSize being efficient for 1-D arrays.
				ArrayVariable resultVar = env.createArray(EsiTypes.STRING, new int[]{ 10 });

				// Use StringTokenizer to split up the argument string, and file each value into the result array.
				StringTokenizer tok = new StringTokenizer(s, delim);
				int N = 0;
				while (tok.hasMoreTokens()) {
					// If result array is too small, double its size.  setDimSize preserves existing values in the array.
					if (N >= resultVar.getSize()) {
						resultVar.setDimSize(2 * resultVar.getSize());
					}

					// Store and count the results.
					resultVar.getValueObj(N).setValue(tok.nextToken());
					N += 1;
				}

				// Trim the result array back down to the correct size and return it.
				resultVar.setDimSize(N);
				return resultVar;
			}
			catch (ClassCastException ex) {
				throw new CalcException(ex, new IString(CLASS, 34379, "Expected argument Variable with different Structure: {0}", ex.getMessage()));
			}
		}
	}
}

