/*
 * 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.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.encog.EncogError;
import org.encog.ml.ea.exception.EARuntimeError;
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.util.csv.CSVFormat;

/**
 * Every EncogProgram must belong to a context. When programs are in a
 * population, they must all share a common context. The context defines
 * attributes that are common to all programs. The following information is
 * stored in a context.
 * 
 * The number formatting used. Namely, what type of radix point should strings
 * be parsed/rendered to.
 * 
 * The functions, or opcodes, that are available to the program. This defines
 * the set of functions & operators that a program might use. For an Encog
 * Program all operators are treated as functions internally. A operator is
 * essentially a shortcut notation for common functions.
 * 
 * The defined variables. These variables are constant for the run of the
 * program, but typically change for each run. They are essentially the
 * variables that make up an algebraic expression.
 * 
 * Finally, the return value mapping for the programs.
 */
public class EncogProgramContext implements Serializable {
	/**
	 * The serial id.
	 */
	private static final long serialVersionUID = 1L;

	/**
	 * The number formatting used. Namely, what type of radix point should
	 * strings be parsed/rendered to.
	 */
	private final CSVFormat format;

	/**
	 * The functions, or opcodes, that are available to the program. This
	 * defines the set of functions & operators that a program might use. For an
	 * Encog Program all operators are treated as functions internally. A
	 * operator is essentially a shortcut notation for common functions.
	 */
	private final FunctionFactory functions;

	/**
	 * The defined variables. These variables are constant for the run of the
	 * program, but typically change for each run. They are essentially the
	 * variables that make up an algebraic expression.
	 */
	private final List<VariableMapping> definedVariables = new ArrayList<VariableMapping>();

	/**
	 * Lookup map for the defined variables.
	 */
	private final Map<String, VariableMapping> map = new HashMap<String, VariableMapping>();

	/**
	 * The return value mapping for the programs.
	 */
	private VariableMapping result = new VariableMapping(null,
			ValueType.floatingType);

	/**
	 * Construct the context with an English number format and an empty function
	 * factory.
	 */
	public EncogProgramContext() {
		this(CSVFormat.ENGLISH, new FunctionFactory());
	}

	/**
	 * Construct a context with the specified number format and an empty
	 * function factory.
	 * 
	 * @param format
	 *            The format.
	 */
	public EncogProgramContext(final CSVFormat format) {
		this(format, new FunctionFactory());
	}

	/**
	 * Construct the context with the specified format and function factory.
	 * 
	 * @param theFormat
	 *            The format.
	 * @param theFunctions
	 *            The function factory.
	 */
	public EncogProgramContext(final CSVFormat theFormat,
			final FunctionFactory theFunctions) {
		this.format = theFormat;
		this.functions = theFunctions;
	}

	/**
	 * Clear the defined variables.
	 */
	public void clearDefinedVariables() {
		this.definedVariables.clear();
		this.map.clear();
	}

	/**
	 * Clone a branch of the program from the specified node.
	 * 
	 * @param targetProgram
	 *            The program that this branch will be "grafted" into.
	 * @param sourceBranch
	 *            The branch to clone, from the source program.
	 * @return The cloned branch.
	 */
	public ProgramNode cloneBranch(final EncogProgram targetProgram,
			final ProgramNode sourceBranch) {
		if (sourceBranch == null) {
			throw new EncogError("Can't clone null branch.");
		}

		final String name = sourceBranch.getName();

		// create any subnodes
		final ProgramNode[] args = new ProgramNode[sourceBranch.getChildNodes()
				.size()];
		for (int i = 0; i < args.length; i++) {
			args[i] = cloneBranch(targetProgram, (ProgramNode) sourceBranch
					.getChildNodes().get(i));
		}

		final ProgramNode result = targetProgram.getContext().getFunctions()
				.factorProgramNode(name, targetProgram, args);

		// now copy the expression data for the node
		for (int i = 0; i < sourceBranch.getData().length; i++) {
			result.getData()[i] = new ExpressionValue(sourceBranch.getData()[i]);
		}

		// return the new node
		return result;
	}

	/**
	 * Clone an entire program, keep the same context.
	 * 
	 * @param sourceProgram
	 *            The source program.
	 * @return The cloned program.
	 */
	public EncogProgram cloneProgram(final EncogProgram sourceProgram) {
		final ProgramNode rootNode = sourceProgram.getRootNode();
		final EncogProgram result = new EncogProgram(this);
		result.setRootNode(cloneBranch(result, rootNode));
		return result;
	}

	/**
	 * Create a new program, using this context.
	 * 
	 * @param expression
	 *            The common expression to compile.
	 * @return The resulting program.
	 */
	public EncogProgram createProgram(final String expression) {
		final EncogProgram result = new EncogProgram(this);
		result.compileExpression(expression);
		return result;
	}

	/**
	 * Define the specified variable as floating point.
	 * 
	 * @param theName
	 *            The variable name to define.
	 */
	public void defineVariable(final String theName) {
		defineVariable(theName, ValueType.floatingType, 0, 0);
	}

	/**
	 * Define the specified variable as the specified type. Don't use this for
	 * enums.
	 * 
	 * @param theName
	 *            The name of the variable.
	 * @param theVariableType
	 *            The variable type.
	 */
	public void defineVariable(final String theName,
			final ValueType theVariableType) {
		defineVariable(theName, theVariableType, 0, 0);
	}

	/**
	 * Define a variable.
	 * 
	 * @param theName
	 *            The name of the variable.
	 * @param theVariableType
	 *            The type of variable.
	 * @param theEnumType
	 *            The enum type, not used if not an enum type.
	 * @param theEnumValueCount
	 *            The number of values for the enum, not used if not an enum
	 *            type.
	 */
	public void defineVariable(final String theName,
			final ValueType theVariableType, final int theEnumType,
			final int theEnumValueCount) {
		final VariableMapping mapping = new VariableMapping(theName,
				theVariableType, theEnumType, theEnumValueCount);
		defineVariable(mapping);

	}

	/**
	 * Define a variable, based on a mapping.
	 * 
	 * @param mapping
	 *            The variable mapping.
	 */
	public void defineVariable(final VariableMapping mapping) {
		if (this.map.containsKey(mapping.getName())) {
			throw new EARuntimeError("Variable " + mapping.getName()
					+ " already defined.");
		}
		this.map.put(mapping.getName(), mapping);
		this.definedVariables.add(mapping);
	}

	/**
	 * Find all of the variables of the specified types.
	 * 
	 * @param desiredTypes
	 *            The types to look for.
	 * @return The variables that matched the specified types.
	 */
	public List<VariableMapping> findVariablesByTypes(
			final List<ValueType> desiredTypes) {
		final List<VariableMapping> result = new ArrayList<VariableMapping>();

		for (final VariableMapping mapping : this.definedVariables) {
			if (desiredTypes.contains(mapping.getVariableType())) {
				result.add(mapping);
			}
		}

		return result;
	}

	/**
	 * @return The defined variables.
	 */
	public List<VariableMapping> getDefinedVariables() {
		return this.definedVariables;
	}

	/**
	 * Get the enum ordinal count for the specified enumeration type.
	 * 
	 * @param enumType
	 *            The enumeration type.
	 * @return The ordinal count for the specified enumeration type.
	 */
	public int getEnumCount(final int enumType) {

		// make sure we consider the result
		if (this.result.getVariableType() == ValueType.enumType
				&& this.result.getEnumType() == enumType) {
			return this.result.getEnumValueCount();
		}

		for (final VariableMapping mapping : this.definedVariables) {
			if (mapping.getVariableType() == ValueType.enumType) {
				if (mapping.getEnumType() == enumType) {
					return mapping.getEnumValueCount();
				}
			}
		}
		throw new EARuntimeError("Undefined enum type: " + enumType);
	}

	/**
	 * @return The number formatting used. Namely, what type of radix point
	 *         should strings be parsed/rendered to.
	 */
	public CSVFormat getFormat() {
		return this.format;
	}

	/**
	 * @return The functions, or opcodes, that are available to the program.
	 *         This defines the set of functions & operators that a program
	 *         might use. For an Encog Program all operators are treated as
	 *         functions internally. A operator is essentially a shortcut
	 *         notation for common functions.
	 */
	public FunctionFactory getFunctions() {
		return this.functions;
	}

	/**
	 * Get the max enum type for all defined variables.
	 * 
	 * @return The max enumeration type.
	 */
	public int getMaxEnumType() {
		int r = -1;

		// make sure we consider the result
		if (this.result.getVariableType() == ValueType.enumType) {
			r = this.result.getEnumType();
		}

		// loop over all mappings and find the max enum type
		for (final VariableMapping mapping : this.definedVariables) {
			if (mapping.getVariableType() == ValueType.enumType) {
				r = Math.max(r, mapping.getEnumType());
			}
		}

		// if we did not find one then there are no enum types
		if (r == -1) {
			throw new EARuntimeError("No enum types defined in context.");
		}

		return r;
	}

	/**
	 * @return the result
	 */
	public VariableMapping getResult() {
		return this.result;
	}

	/**
	 * @return True, if enums are defined.
	 */
	public boolean hasEnum() {
		if (this.result.getVariableType() == ValueType.enumType) {
			return true;
		}

		for (final VariableMapping mapping : this.definedVariables) {
			if (mapping.getVariableType() == ValueType.enumType) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Load all known functions as opcodes.
	 */
	public void loadAllFunctions() {
		StandardExtensions.createAll(this);
	}

	/**
	 * @param result
	 *            the result to set
	 */
	public void setResult(final VariableMapping result) {
		this.result = result;
	}

}
