/*
 * SubtreeSlide.java
 *
 * Copyright (c) 2002-2015 Alexei Drummond, Andrew Rambaut and Marc Suchard
 *
 * This file is part of BEAST.
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership and licensing.
 *
 * BEAST is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 *  BEAST is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with BEAST; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

package dr.evolution.tree;

import dr.math.MathUtils;

import java.util.ArrayList;

/**
 * A move that selects a subtree and slides it up or down the tree.
 * This move can result in tree rearrangments. With small moves it will affect only
 * a single node height.
 *
 * @author Alexei Drummond
 *
 * @version $Id: SubtreeSlide.java,v 1.7 2005/05/24 20:25:57 rambaut Exp $
 */
public class SubtreeSlide {

	public SubtreeSlide(double size, boolean gaussian) {
		this.size = size;
		this.gaussian = gaussian;
	}

	public double getSize() { return size; }
	public void setSize(double size) { this.size = size; }

	/**
	 * Do a probablistic subtree slide move.
	 * @return the log-transformed hastings ratio
	 */
	public double slideSubtree(MutableTree tree) {

        double logHastingsRatio;

		NodeRef i, newParent, newChild;

		// 1. choose a random node avoiding root
		do {
			i = tree.getNode(MathUtils.nextInt(tree.getNodeCount()));
		} while (tree.getRoot() == i);
		NodeRef iP = tree.getParent(i);
		NodeRef CiP = getOtherChild(tree,iP, i);
		NodeRef PiP = tree.getParent(iP);

		// 2. choose a delta to move
		double delta = getDelta();
		double oldHeight = tree.getNodeHeight(iP);
		double newHeight = oldHeight + delta;

		// 3. if the move is up
		if (delta > 0) {

			// 3.1 if the topology will change
			if (PiP != null && tree.getNodeHeight(PiP) < newHeight) {

				// find new parent
				newParent = PiP; newChild = iP;
				while (tree.getNodeHeight(newParent) < newHeight) {
					newChild = newParent;
					newParent = tree.getParent(newParent);
					if (newParent == null) break;
				}

				tree.beginTreeEdit();

				// 3.1.1 if creating a new root
				if (tree.isRoot(newChild)) {
					tree.removeChild(iP, CiP); tree.removeChild(PiP, iP);
					tree.addChild(iP, newChild); tree.addChild(PiP, CiP);
					tree.setRoot(iP);
					//System.err.println("Creating new root!");
				}
				// 3.1.2 no new root
				else {
					tree.removeChild(iP, CiP); tree.removeChild(PiP, iP);
					tree.removeChild(newParent, newChild);
					tree.addChild(iP, newChild); tree.addChild(PiP, CiP);
					tree.addChild(newParent, iP);
					//System.err.println("No new root!");
				}

				tree.setNodeHeight(iP, newHeight);

                tree.endTreeEdit();

				// 3.1.3 count the hypothetical sources of this destination.
				int possibleSources = intersectingEdges(tree, newChild, oldHeight, null);
				//System.out.println("possible sources = " + possibleSources);

				logHastingsRatio = Math.log(1.0/(double)possibleSources);

			} else {
				// just change the node height
				tree.setNodeHeight(iP, newHeight);
				logHastingsRatio = 0.0;
			}
		}
		// 4 if we are sliding the subtree down.
		else {

			// 4.0 is it a valid move?
			if (tree.getNodeHeight(i) > newHeight) {
				return Double.NEGATIVE_INFINITY;
			}

			// 4.1 will the move change the topology
			if (tree.getNodeHeight(CiP) > newHeight) {

				ArrayList newChildren = new ArrayList();
				int possibleDestinations = intersectingEdges(tree, CiP, newHeight, newChildren);

				// if no valid destinations then return a failure
				if (newChildren.size() == 0) { return Double.NEGATIVE_INFINITY; }

				// pick a random parent/child destination edge uniformly from options
				int childIndex = MathUtils.nextInt(newChildren.size());
				newChild = (NodeRef)newChildren.get(childIndex);
				newParent = tree.getParent(newChild);

				tree.beginTreeEdit();

				// 4.1.1 if iP was root
				if (tree.isRoot(iP)) {
					// new root is CiP
					tree.removeChild(iP, CiP); tree.removeChild(newParent, newChild);
					tree.addChild(iP, newChild); tree.addChild(newParent, iP);
					tree.setRoot(CiP);
					//System.err.println("DOWN: Creating new root!");
				} else {
					tree.removeChild(iP, CiP); tree.removeChild(PiP, iP);
					tree.removeChild(newParent, newChild);
					tree.addChild(iP, newChild); tree.addChild(PiP, CiP);
					tree.addChild(newParent, iP);
					//System.err.println("DOWN: no new root!");
				}

				tree.setNodeHeight(iP, newHeight);

                tree.endTreeEdit();
				
				logHastingsRatio = Math.log((double)possibleDestinations);
			} else {
				tree.setNodeHeight(iP, newHeight);
				logHastingsRatio = 0.0;
			}
		}

        return logHastingsRatio;
	}

	private double getDelta() {
		if (!gaussian) {
			return (MathUtils.nextDouble() * size) - (size/2.0);
		} else {
			return MathUtils.nextGaussian() * size;
		}
	}

	private int intersectingEdges(Tree tree, NodeRef node, double height, ArrayList directChildren) {

		NodeRef parent = tree.getParent(node);

		if (tree.getNodeHeight(parent) < height) return 0;

		if (tree.getNodeHeight(node) < height) {
			if (directChildren != null) directChildren.add(node);
			return 1;
		}

		int count = 0;
		for (int i = 0; i < tree.getChildCount(node); i++) {
			count += intersectingEdges(tree, tree.getChild(node, i), height, directChildren);
		}
		return count;
	}

	/**
	 * @return the other child of the given parent.
	 */
	private NodeRef getOtherChild(Tree tree, NodeRef parent, NodeRef child) {

		if (tree.getChild(parent, 0) == child) {
			return tree.getChild(parent, 1);
		} else {
			return tree.getChild(parent, 0);
		}
	}

	private double size = 1.0;
	private boolean gaussian = false;
}
