/*
 * Encog(tm) Core v3.2 - Java Version
 * http://www.heatonresearch.com/encog/
 * https://github.com/encog/encog-java-core
 
 * Copyright 2008-2013 Heaton Research, Inc.
 *
 * 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.
 *   
 * For more information on Heaton Research copyrights, licenses 
 * and trademarks visit:
 * http://www.heatonresearch.com/copyright
 */
package org.encog.ml.prg;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.encog.ml.MLError;
import org.encog.ml.MLRegression;
import org.encog.ml.data.MLData;
import org.encog.ml.data.MLDataSet;
import org.encog.ml.data.basic.BasicMLData;
import org.encog.ml.ea.exception.EACompileError;
import org.encog.ml.ea.exception.EARuntimeError;
import org.encog.ml.ea.genome.BasicGenome;
import org.encog.ml.ea.genome.Genome;
import org.encog.ml.prg.expvalue.ExpressionValue;
import org.encog.ml.prg.expvalue.ValueType;
import org.encog.ml.prg.extension.FunctionFactory;
import org.encog.ml.prg.extension.StandardExtensions;
import org.encog.ml.prg.train.PrgPopulation;
import org.encog.ml.tree.traverse.tasks.TaskGetNodeIndex;
import org.encog.ml.tree.traverse.tasks.TaskReplaceNode;
import org.encog.parse.expression.common.ParseCommonExpression;
import org.encog.parse.expression.common.RenderCommonExpression;
import org.encog.parse.expression.epl.ParseEPL;
import org.encog.parse.expression.epl.RenderEPL;
import org.encog.parse.expression.rpn.RenderRPN;
import org.encog.util.simple.EncogUtility;

/**
 * Holds an Encog Programming Language (EPL) program. A Encog program is
 * internally represented as a tree structure. It can be rendered to a variety
 * of forms, such as RPN, common infix expressions, or Latex. The Encog
 * Workbench also allows display as a graphical tree. An Encog Program is both a
 * genome and phenome. No decoding is necessary.
 * 
 * Every Encog Program has a context. The context is the same for every Encog
 * Program in a population. The context defines which opcodes should be used, as
 * well as the defined variables.
 * 
 * The actual values for the variables are not stored in the context. Rather
 * they are stored in a variable holder. Each program usually has its own
 * variable holder, though it is possible to share.
 */
public class EncogProgram extends BasicGenome implements MLRegression, MLError {

	/**
	 * The serial id.
	 */
	private static final long serialVersionUID = 1L;

	/**
	 * Parse the specified program, or expression, and return the result. No
	 * variables can be defined for this as a default context is used. The
	 * result is returned as a boolean.
	 * 
	 * @param str
	 *            The program expression.
	 * @return The value the expression was evaluated to.
	 */
	public static boolean parseBoolean(final String str) {
		final EncogProgram holder = new EncogProgram(str);
		return holder.evaluate().toBooleanValue();
	}

	/**
	 * Parse the specified program, or expression, and return the result. No
	 * variables can be defined for this as a default context is used. The
	 * result is returned as a boolean.
	 * 
	 * @param str
	 *            The program expression.
	 * @return The value the expression was evaluated to.
	 */
	public static ExpressionValue parseExpression(final String str) {
		final EncogProgram holder = new EncogProgram(str);
		return holder.evaluate();
	}

	/**
	 * Parse the specified program, or expression, and return the result. No
	 * variables can be defined for this as a default context is used. The
	 * result is returned as a float.
	 * 
	 * @param str
	 *            The program expression value.
	 * @return The value the expression was evaluated to.
	 */
	public static double parseFloat(final String str) {
		final EncogProgram holder = new EncogProgram(str);
		return holder.evaluate().toFloatValue();
	}

	/**
	 * Parse the specified program, or expression, and return the result. No
	 * variables can be defined for this as a default context is used. The
	 * result is returned as a string.
	 * 
	 * @param str
	 *            The program expression value.
	 * @return The value the expression was evaluated to.
	 */
	public static String parseString(final String str) {
		final EncogProgram holder = new EncogProgram(str);
		return holder.evaluate().toStringValue();
	}

	/**
	 * The variables that will be used by this Encog program.
	 */
	private EncogProgramVariables variables = new EncogProgramVariables();

	/**
	 * The context that this Encog program executes in, the context is typically
	 * shared with other Encog programs.
	 */
	private EncogProgramContext context = new EncogProgramContext();

	/**
	 * The root node of the program.
	 */
	private ProgramNode rootNode;
	
	/**
	 * Holds extra data that might be needed by user extended opcodes.
	 */
	private Map<String,Object> extraData = new HashMap<String,Object>();

	/**
	 * Construct the Encog program and create a default context and variable
	 * holder. Use all available opcodes.
	 */
	public EncogProgram() {
		this(new EncogProgramContext(), new EncogProgramVariables());
		StandardExtensions.createAll(this.context);
	}

	/**
	 * Construct the Encog program with the specified context, but create a new
	 * variable holder.
	 * 
	 * @param theContext
	 *            The context.
	 */
	public EncogProgram(final EncogProgramContext theContext) {
		this(theContext, new EncogProgramVariables());
	}

	/**
	 * Construct an Encog program using the specified context and variable
	 * holder.
	 * 
	 * @param theContext
	 *            The context.
	 * @param theVariables
	 *            The variable holder.
	 */
	public EncogProgram(final EncogProgramContext theContext,
			final EncogProgramVariables theVariables) {
		this.context = theContext;
		this.variables = theVariables;

		// define variables
		for (final VariableMapping v : this.context.getDefinedVariables()) {
			this.variables.defineVariable(v);
		}
	}

	/**
	 * Construct an Encog program using the specified expression, but create an
	 * empty context and variable holder.
	 * 
	 * @param expression
	 *            The expression.
	 */
	public EncogProgram(final String expression) {
		this();
		compileExpression(expression);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public double calculateError(final MLDataSet data) {
		return EncogUtility.calculateRegressionError(this, data);
	}

	/**
	 * Compile the specified EPL into an actual program node structure, for
	 * later execution.
	 * 
	 * @param code The code to compile.
	 * @return The root node.
	 */
	public ProgramNode compileEPL(final String code) {
		final ParseEPL parser = new ParseEPL(this);
		this.rootNode = parser.parse(code);
		return this.rootNode;
	}

	/**
	 * Compile the specified expression.
	 * 
	 * @param expression
	 *            The expression.
	 * @return The program node that this was compiled into.
	 */
	public ProgramNode compileExpression(final String expression) {
		final ParseCommonExpression parser = new ParseCommonExpression(this);
		this.rootNode = parser.parse(expression);
		return this.rootNode;
	}

	/**
	 * Compute the output from the input MLData. The individual values of the
	 * input will be mapped to the variables defined in the context. The order
	 * is the same between the input and the defined variables. The input will
	 * be mapped to the appropriate types. Enums will use their ordinal number.
	 * The result will be a single number MLData.
	 * 
	 * @param input
	 *            The input to the program.
	 * @return A single numer MLData.
	 */
	@Override
	public MLData compute(final MLData input) {
		if (input.size() != getInputCount()) {
			throw new EACompileError("Invalid input count.");
		}

		for (int i = 0; i < input.size(); i++) {
			this.variables.setVariable(i, input.getData(i));
		}

		final ExpressionValue v = this.rootNode.evaluate();
		final VariableMapping resultMapping = getResultType();

		final MLData result = new BasicMLData(1);
		boolean success = false;

		switch (resultMapping.getVariableType()) {
		case floatingType:
			if (v.isNumeric()) {
				result.setData(0, v.toFloatValue());
				success = true;
			}
			break;
		case stringType:
			result.setData(0, v.toFloatValue());
			success = true;
			break;
		case booleanType:
			if (v.isBoolean()) {
				result.setData(0, v.toBooleanValue() ? 1.0 : 0.0);
				success = true;
			}
			break;
		case intType:
			if (v.isNumeric()) {
				result.setData(0, v.toIntValue());
				success = true;
			}
			break;
		case enumType:
			if (v.isEnum()) {
				result.setData(0, v.toIntValue());
				success = true;
			}
			break;
		}

		if (!success) {
			throw new EARuntimeError("EncogProgram produced "
					+ v.getExpressionType().toString() + " but "
					+ resultMapping.getVariableType().toString()
					+ " was expected.");
		}

		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void copy(final Genome source) {
		// not needed, already a genome
	}

	/**
	 * @return The string as a common "infix" expression.
	 */
	public String dumpAsCommonExpression() {
		final RenderCommonExpression render = new RenderCommonExpression();
		return render.render(this);
	}

	/**
	 * Execute the program and return the result.
	 * 
	 * @return The result of running the program.
	 */
	public ExpressionValue evaluate() {
		return this.rootNode.evaluate();
	}

	/**
	 * Find the specified node by index. The tree is traversed to do this. This
	 * is typically used to select a random node.
	 * 
	 * @param index
	 *            The index being sought.
	 * @return The program node found.
	 */
	public ProgramNode findNode(final int index) {
		return (ProgramNode) TaskGetNodeIndex.process(index, this.rootNode);
	}

	/**
	 * @return The string as an EPL expression. EPL is the format that
	 *         EncogPrograms are usually persisted as.
	 */
	public String generateEPL() {
		final RenderEPL render = new RenderEPL();
		return render.render(this);
	}

	/**
	 * @return The program context. The program context may be shared over
	 *         multiple programs.
	 */
	public EncogProgramContext getContext() {
		return this.context;
	}

	/**
	 * @return The function factory from the context.
	 */
	public FunctionFactory getFunctions() {
		return this.context.getFunctions();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getInputCount() {
		return this.variables.size();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getOutputCount() {
		return 1;
	}

	/**
	 * @return The variable mapping for the result type. This is obtained from
	 *         the context.
	 */
	private VariableMapping getResultType() {
		return ((PrgPopulation) getPopulation()).getContext().getResult();
	}

	/**
	 * @return The return type, from the context.
	 */
	public ValueType getReturnType() {
		return this.context.getResult().getVariableType();
	}

	/**
	 * @return The root node of the program.
	 */
	public ProgramNode getRootNode() {
		return this.rootNode;
	}

	/**
	 * @return The variable holder.
	 */
	public EncogProgramVariables getVariables() {
		return this.variables;
	}

	/**
	 * Replace the specified node with another.
	 * 
	 * @param replaceThisNode
	 *            The node to replace.
	 * @param replaceWith
	 *            The node that is replacing that node.
	 */
	public void replaceNode(final ProgramNode replaceThisNode,
			final ProgramNode replaceWith) {
		if (replaceThisNode == this.rootNode) {
			this.rootNode = replaceWith;
		} else {
			TaskReplaceNode
					.process(this.rootNode, replaceThisNode, replaceWith);
		}
	}

	/**
	 * Select a random variable from the defined variables.
	 * 
	 * @param rnd
	 *            A random number generator.
	 * @param desiredTypes
	 *            The desired types that the variable can be.
	 * @return The index of the defined variable, or -1 if unable to define.
	 */
	public int selectRandomVariable(final Random rnd,
			final List<ValueType> desiredTypes) {
		List<VariableMapping> selectionSet = this.context
				.findVariablesByTypes(desiredTypes);
		if (selectionSet.size() == 0
				&& desiredTypes.contains(ValueType.intType)) {
			final List<ValueType> floatList = new ArrayList<ValueType>();
			floatList.add(ValueType.floatingType);
			selectionSet = this.context.findVariablesByTypes(floatList);
		}

		if (selectionSet.size() == 0) {
			return -1;
		}

		final VariableMapping selected = selectionSet.get(rnd
				.nextInt(selectionSet.size()));
		return getContext().getDefinedVariables().indexOf(selected);
	}

	/**
	 * Set the root node for the program.
	 * 
	 * @param theRootNode
	 *            The new root node.
	 */
	public void setRootNode(final ProgramNode theRootNode) {
		this.rootNode = theRootNode;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int size() {
		return this.rootNode.size();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String toString() {
		final RenderRPN render = new RenderRPN();
		final String code = render.render(this);
		final StringBuilder result = new StringBuilder();
		result.append("[EncogProgram: size=");
		result.append(size());
		result.append(", score=");
		result.append(getScore());
		result.append(",code=");
		result.append(code);
		result.append("]");
		return result.toString();
	}

	/**
	 * Get extra data that might be needed by user extended opcodes.
	 * @param name The name the data was stored under.
	 * @return The extra data.
	 */
	public Object getExtraData(final String name) {
		return this.extraData.get(name);
	}
	
	/**
	 * Set extra data that might be needed by extensions.
	 * @param name The name of the data stored.
	 * @param value The data.
	 */
	public void setExtraData(final String name, final Object value) {
		this.extraData.put(name, value);
	}
}
