/*
 * 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.extension;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.encog.ml.ea.exception.EACompileError;
import org.encog.ml.prg.EncogProgram;
import org.encog.ml.prg.EncogProgramContext;
import org.encog.ml.prg.ExpressionError;
import org.encog.ml.prg.ProgramNode;
import org.encog.ml.prg.expvalue.ValueType;

/**
 * A function factory maps the opcodes contained in the EncogOpcodeRegistry into
 * an EncogProgram. You rarely want to train with every available opcode. For
 * example, if you are only looking to produce an equation, you should not make
 * use of the if-statement and logical operators.
 */
public class FunctionFactory implements Serializable {

    /**
     * The serial id.
     */
    private static final long serialVersionUID = 1L;
    /**
     * A map for quick lookup.
     */
    private final Map<String, ProgramExtensionTemplate> templateMap =
            new HashMap<String, ProgramExtensionTemplate>();
    /**
     * The opcodes.
     */
    private final List<ProgramExtensionTemplate> opcodes =
            new ArrayList<ProgramExtensionTemplate>();

    /**
     * Default constructor.
     */
    public FunctionFactory() {
    }

    /**
     * Add an opcode to the function factory. The opcode must exist in the
     * opcode registry.
     * <p/>
     * @param ext
     *            The opcode to add.
     */
    public void addExtension(final ProgramExtensionTemplate ext) {
        addExtension(ext.getName(), ext.getChildNodeCount());
    }

    /**
     * Add an opcode to the function factory from the opcode registry.
     * <p/>
     * @param name
     *             The name of the opcode.
     * @param args
     *             The number of arguments.
     */
    public void addExtension(final String name, final int args) {
        final String key = EncogOpcodeRegistry.createKey(name, args);
        if (!this.templateMap.containsKey(key)) {
            final ProgramExtensionTemplate temp = EncogOpcodeRegistry.INSTANCE
                    .findOpcode(name, args);
            if (temp == null) {
                throw new EACompileError(
                        "Unknown extension " + name + " with " +
                        args + " arguments.");
            }
            this.opcodes.add(temp);
            this.templateMap.put(key, temp);
        }

    }

    /**
     * Factor a new program node, based in a template object.
     * <p/>
     * @param temp
     *                The opcode.
     * @param program
     *                The program.
     * @param args
     *                The arguments for this node.
     * <p/>
     * @return The newly created ProgramNode.
     */
    public ProgramNode factorProgramNode(final ProgramExtensionTemplate temp,
                                         final EncogProgram program,
                                         final ProgramNode[] args) {
        return new ProgramNode(program, temp, args);
    }

    /**
     * Factor a new program node, based on an opcode name and arguments.
     * <p/>
     * @param name
     *                The name of the opcode.
     * @param program
     *                The program to factor for.
     * @param args
     *                The arguements.
     * <p/>
     * @return The newly created ProgramNode.
     */
    public ProgramNode factorProgramNode(final String name,
                                         final EncogProgram program,
                                         final ProgramNode[] args) {

        final String key = EncogOpcodeRegistry.createKey(name, args.length);

        if (!this.templateMap.containsKey(key)) {
            throw new ExpressionError("Undefined function/operator: " + name +
                    " with " + args.length + " args.");
        }

        final ProgramExtensionTemplate temp = this.templateMap.get(key);
        return new ProgramNode(program, temp, args);
    }

    /**
     * Find a function with the specified name.
     * <p/>
     * @param name
     *             The name of the function.
     * <p/>
     * @return The function opcode.
     */
    public ProgramExtensionTemplate findFunction(final String name) {
        for (final ProgramExtensionTemplate opcode : this.opcodes) {
            // only consider functions
            if (opcode.getNodeType() == NodeType.Function) {
                if (opcode.getName().equals(name)) {
                    return opcode;
                }
            }
        }
        return null;
    }

    /**
     * Find all opcodes that match the search criteria.
     * <p/>
     * @param types
     *                        The return types to consider.
     * @param context
     *                        The program context.
     * @param includeTerminal
     *                        True, to include the terminals.
     * @param includeFunction
     *                        True, to include the functions.
     * <p/>
     * @return The opcodes found.
     */
    public List<ProgramExtensionTemplate> findOpcodes(
            final List<ValueType> types, final EncogProgramContext context,
            final boolean includeTerminal, final boolean includeFunction) {
        final List<ProgramExtensionTemplate> result =
                new ArrayList<ProgramExtensionTemplate>();

        for (final ProgramExtensionTemplate temp : this.opcodes) {
            for (final ValueType rtn : types) {
                // it is a possible return type, but given our variables, is it
                // possible
                if (temp.isPossibleReturnType(context, rtn)) {
                    if (temp.getChildNodeCount() == 0 && includeTerminal) {
                        result.add(temp);
                    } else if (includeFunction) {
                        result.add(temp);
                    }
                }
            }
        }

        return result;
    }

    /**
     * This method is used when parsing an expression. Consider x>=2. The parser
     * first sees the > symbol. But it must also consider the =. So we first
     * look for a 2-char operator, in this case there is one.
     * <p/>
     * @param ch1
     *            The first character of the potential operator.
     * @param ch2
     *            The second character of the potential operator. Zero if none.
     * <p/>
     * @return The expression template for the operator found.
     */
    public ProgramExtensionTemplate findOperator(final char ch1, final char ch2) {
        ProgramExtensionTemplate result = null;

        // if ch2==0 then we are only looking for a single char operator.
        // this is rare, but supported.
        if (ch2 == 0) {
            return findOperatorExact("" + ch1);
        }

        // first, see if we can match an operator with both ch1 and ch2
        result = findOperatorExact("" + ch1 + ch2);

        if (result == null) {
            // okay no 2-char operators matched, so see if we can find a single
            // char
            result = findOperatorExact("" + ch1);
        }

        // return the operator if we have one.
        return result;
    }

    /**
     * Find an exact match on opcode.
     * <p/>
     * @param str
     *            The string to match.
     * <p/>
     * @return The opcode found.
     */
    private ProgramExtensionTemplate findOperatorExact(final String str) {
        for (final ProgramExtensionTemplate opcode : this.opcodes) {
            // only consider non-unary operators
            if (opcode.getNodeType() == NodeType.OperatorLeft || opcode
                    .getNodeType() == NodeType.OperatorRight) {
                if (opcode.getName().equals(str)) {
                    return opcode;
                }
            }
        }
        return null;
    }

    /**
     * Get the specified opcode.
     * <p/>
     * @param theOpCode
     *                  The opcode index.
     * <p/>
     * @return The opcode.
     */
    public ProgramExtensionTemplate getOpCode(final int theOpCode) {
        return this.opcodes.get(theOpCode);
    }

    /**
     * @return The opcode list.
     */
    public List<ProgramExtensionTemplate> getOpCodes() {
        return this.opcodes;
    }

    /**
     * @return the templateMap
     */
    public Map<String, ProgramExtensionTemplate> getTemplateMap() {
        return this.templateMap;
    }

    /**
     * Determine if an opcode is in the function factory.
     * <p/>
     * @param name
     *             The name of the opcode.
     * @param l
     *             The argument count for the opcode.
     * <p/>
     * @return True if the opcode exists.
     */
    public boolean isDefined(final String name, final int l) {
        final String key = EncogOpcodeRegistry.createKey(name, l);
        return this.templateMap.containsKey(key);
    }

    /**
     * @return The number of opcodes.
     */
    public int size() {
        return this.opcodes.size();
    }
}
