package dr.evomodel.tree;

import dr.evolution.tree.NodeRef;
import dr.evolution.tree.Tree;
import dr.evolution.tree.TreeNodeFilter;
import dr.evolution.util.TaxonList;
import dr.inference.model.AbstractModel;
import dr.inference.model.Model;
import dr.inference.model.Variable;

import java.util.BitSet;
import java.util.HashSet;
import java.util.Set;

/**
 * @author Marc A. Suchard
 * @author Andrew Rambaut
 */
public class BackboneNodeFilter extends AbstractModel implements TreeNodeFilter {

    public BackboneNodeFilter(String name, Tree tree, TaxonList taxonList, boolean excludeClade, boolean includeStem) {
        super(name);

        this.tree = tree;
        this.excludeClade = excludeClade;
        this.includeStem = includeStem;

        backboneSet = new HashSet<NodeRef>();

        try {
            tipSet = Tree.Utils.getTipsForTaxa(this.tree, taxonList);
        } catch (Tree.MissingTaxonException e) {
            e.printStackTrace();
        }

        if (tree instanceof TreeModel) {
            addModel((TreeModel) tree);
        }

        backboneSetKnown = false;
    }

    public boolean includeNode(Tree tree, NodeRef node) {

        if (!backboneSetKnown) {
            computeBackBoneMap();
            backboneSetKnown = true;
        }
        return backboneSet.contains(node);
    }

    private void computeBackBoneMap() {
        backboneSet.clear();
        recursivelyComputeBackBoneMap(tree, tree.getRoot());
    }

    private boolean recursivelyComputeBackBoneMap(Tree tree, NodeRef node) {
       boolean onBackBone = false;

        if (tree.isExternal(node)) {
            if (tipSet.get(node.getNumber())) {
                onBackBone = true;
            }
        } else {
            for (int i = 0; i < tree.getChildCount(node); i++) {
                NodeRef child = tree.getChild(node, i);
                if (recursivelyComputeBackBoneMap(tree, child)) {
                    // if any of the desendents are back bone then this node is too
                    onBackBone = true;
                }
            }
        }

        if (onBackBone) {
            recursivelyPruneStemAndClade(tree, node, includeStem, excludeClade);
            return true;
        }
        return false;
    }

    private void recursivelyPruneStemAndClade(Tree tree, NodeRef node,
                                              boolean includeStem, boolean excludeClade) {

        if (!tree.isExternal(node) && !excludeClade) {
            for (int i = 0; i < tree.getChildCount(node); i++) {
                NodeRef child = tree.getChild(node, i);
                recursivelyPruneStemAndClade(tree, child, true, true);
            }
        }

        if (includeStem && !backboneSet.contains(node)) {
            backboneSet.add(node);
        }
    }

    @Override
    protected void handleModelChangedEvent(Model model, Object object, int index) {

        if (model == tree) {
            backboneSetKnown = false;
        }

//            if (model == tree) {
//                if (object instanceof TreeModel.TreeChangedEvent) {
//                    TreeModel.TreeChangedEvent event = (TreeModel.TreeChangedEvent) object;
//                    if (event.isHeightChanged()) {
//                        // Do nothing
//                        return;
//                    }
//                    if (event.isNodeParameterChanged()) {
//                        // Do nothing
//                        return;
//                    } // else a topology change (?)
//                    backboneSetKnown = false;
//                    return;
//                }
//            }
    }

    @Override
    protected void handleVariableChangedEvent(Variable variable, int index, Variable.ChangeType type) {
    }

    @Override
    protected void storeState() {
    }

    @Override
    protected void restoreState() {
    }

    @Override
    protected void acceptState() {
    }

    private boolean backboneSetKnown;
    private final Set<NodeRef> backboneSet;
    private BitSet tipSet;

    private final Tree tree;
    private final boolean includeStem;
    private final boolean excludeClade;
}