package ac.essex.gp.treebuilders;

import ac.essex.gp.params.*;
import ac.essex.gp.tree.Node;
import ac.essex.gp.tree.Terminal;
import ac.essex.gp.individuals.Individual;
import ac.essex.gp.nodes.ercs.BasicERC;

import java.util.Vector;

/**
 *
 * <p>
 * Builds a tree up to a maximum number of nodes. This allows irregular tree shapes to be created.
 * </p>
 * <p>
 * Create tree implements range typing and Node child constraints
 * </p>
 *
 * @author Olly Oechsle, University of Essex, Date: 15-Jan-2007
 * @version 1.0
 */
public class NodeSizeBuilder extends TreeBuilder {

    public Node createTree(GPParams params) {
        return createTree(params, params.getReturnType(), params.getMaxTreeSize());
    }

    private int size;

    public Node createTree(GPParams params, int returnType , int maxSize) {

        // get a node that returns the correct type for the problem
        Node parent;
        if (maxSize <= 1) {
            parent = params.getTerminalNodeByType(returnType, RangeTypes.DONT_CARE).getInstance();
        } else {
            parent = params.getNonTerminalNodeByType(returnType).getInstance();
        }

        size = 1;

        // don't always grow to max size
        //maxSize = (int) (maxSize * (Math.random()));

        growTree(parent, parent, params, maxSize, params.getTreeConstraints());

        if (parent.getTreeSize() > maxSize) {
            //System.out.println("ERROR IN CREATE TREE - TREES ARE TOO BIG: " + parent.getTreeSize());
        }

        return parent;

    }

    /**
     * Creates a tree up to a given size.
     *
     */
    public void growTree(Node root, Node parent, GPParams params, int maxSize, Vector<TreeConstraint> treeConstraints)  {

        size += parent.countChildren();

        boolean keepGrowing = false;

        // if all tree constraints are not matched by this tree, keep building it until they are
        // this hack allows us to ensure that a certain number of each node is included.
        // TODO: Either test or remove this dubious feature
        for (int i = 0; i < treeConstraints.size(); i++) {
            TreeConstraint treeConstraint = treeConstraints.elementAt(i);
            int nodeCount = root.countNodes(treeConstraint.node);
            if (nodeCount < treeConstraint.minimumRequired) {
                keepGrowing = true;
            }
        }


        // create child nodes to fill in the parent's children
        for (int i = 0; i < parent.countChildren(); i++) {

            // what return type should this child be?
            int childReturnType = parent.getChildType(i);


            // it is possible for the parent node to dictate whether the child
            // should be an ERC, a Terminal, or anything that matches the return
            // type, by default this is anything, unless NodeChildConstraints is enabled
            // and the node specifically states what it wants.

            int childType;
            if (params.isNodeChildConstraintsEnabled()) {
                childType = parent.getChildConstraint(i);
            } else {
                // by default I go for *any* function, unless the node says otherwise
                childType = ChildConstraints.NO_CONSTRAINT;
            }

            // see if we've included enough of the constrained node
            //int numberOfInstances = parent.countNodes(constrainedNode);

            // if we're close to the maximum size, go for terminals only.
            if (!keepGrowing && size > maxSize - 4) childType = ChildConstraints.MUST_BE_TERMINAL;

            // Start creating the child here
            NodeParams child;
            switch (childType) {
                case ChildConstraints.MUST_BE_TERMINAL:
                    // get terminals to finish off the tree
                    child = params.getTerminalNodeByType(childReturnType, RangeTypes.DONT_CARE);
                    break;
                case ChildConstraints.MUST_BE_ERC:
                    // what kind of ERC do we need to add? Consult the parent again
                    int childRangeType = parent.getChildRangeType(i);
                    if (childRangeType != RangeTypes.DONT_CARE) {
                        // a specific range type is requested
                        NodeParams erc = params.getERCByRangeType(childReturnType, childRangeType);
                        if (erc == null) {
                            System.err.println("Cant get ERC with return type: " + childReturnType + " and Range Type: " + RangeTypes.rangeTypeToString(childRangeType));
                        }
                        child = erc;
                    } else {
                        // any ERC will do
                        child = params.getERCByType(childReturnType);
                    }
                    break;
                default:
                    // any child will do
                    child = params.getNodeByType(childReturnType);
            }

            if (child == null) {
                throw new RuntimeException("BUG in NodeSizeBuilder: Could not find a suitable child.");
            }

            Node childNode = child.getInstance();

            parent.addChild(childNode, i);

            growTree(root, childNode, params, maxSize, treeConstraints);

        }

    }

    public Node produceMutatedTree(Individual ind, Node originalBranch, GPParams params) {

        // if we take away this branch, how big will the individual be?
        int remainder = ind.getTree().getTreeSize() - originalBranch.getTreeSize();

        // now, knowing the maximum allowed size, we know how big the new branch can be
        int maxSize = params.getMaxTreeSize() - remainder;

        // if its an ERC - replace with another ERC with the same range type
        if (originalBranch instanceof BasicERC) {
            int rangeType = ((BasicERC) originalBranch).getRangeID();
            NodeParams p = params.getERCByRangeType(originalBranch.getReturnType(), rangeType);
            if (p == null) {
                // just get any ERC
                p = params.getERCByRangeType(originalBranch.getReturnType(), RangeTypes.DONT_CARE);
            }
            return p.getInstance();
        }

        // if its a terminal - replace with another Terminal with the same range type
        if (originalBranch instanceof Terminal) {
            int rangeType = ((Terminal) originalBranch).getRangeID();
            NodeParams p = params.getTerminalNodeByType(originalBranch.getReturnType(), rangeType);
            if (p != null) {
                return p.getInstance();
            } else {
                throw new RuntimeException("ProduceMutatedTree: Can't get Terminal by range type: " + NodeParams.returnTypeToString(originalBranch.getReturnType()) + ", " + RangeTypes.rangeTypeToString(rangeType));
            }
        }//*/

        // create and return a replacement tree
        return createTree(params, originalBranch.getReturnType(), maxSize);

    }

}
