/*
 * StructuredCoalescent.java
 *
 * Copyright (C) 2002-2009 Alexei Drummond and Andrew Rambaut
 *
 * 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.coalescent.structure;

import dr.evolution.colouring.ColourChangeMatrix;
import dr.evolution.colouring.TreeColouring;
import dr.evolution.tree.NodeRef;
import dr.evolution.tree.Tree;

/**
 * @author Alexei Drummond
 * @author Gerton Lunter
 * @author Andrew Rambaut
 * @version $Id: StructuredCoalescent.java,v 1.17 2006/09/11 09:33:01 gerton Exp $
 */
public class StructuredCoalescent {

    static double tinyTime = 1.0e-6;        // make sure rounding errors don't influence population sizes (for Bayesian Skyline)

    public StructuredCoalescent() {

    }

    /**
     * @param intervals
     * @return the log likelihood
     */
//    public double calculateLogLikelihood(TreeColouring treeColouring, StructuredIntervalList intervals, ColourChangeMatrix mm, double[] N) {
    public double calculateLogLikelihood(TreeColouring treeColouring, StructuredIntervalList intervals, ColourChangeMatrix mm, MetaPopulation mp) {

        double logL = 0;
        Tree tree = treeColouring.getTree();

        double _totalIntegratedRate = 0.0;  /* debugging */

        // include the probability of the leaf colouring
        for (int i = 0; i < tree.getExternalNodeCount(); i++) {

            NodeRef leafNode = tree.getExternalNode(i);
            logL += Math.log(mm.getEquilibrium(treeColouring.getNodeColour(leafNode)));

        }

        double currentTime = 0.0;

        // walk up the tree
        for (int i = 0; i < intervals.getIntervalCount(); i++) {

            // integral of exit rate over time interval
            double intensity = 0.0;

            // get length of current interval
            double time = intervals.getInterval(i);

            // calculate the integrated exit rate
            for (int j = 0; j < intervals.getPopulationCount(); j++) {

                int lineages = intervals.getLineageCount(i, j);

                // coalescent contribution
                //exitRate += lineages * (lineages-1) / (2*N[j]);

                intensity += (lineages * (lineages - 1) / 2.0) * mp.getIntegral(currentTime, currentTime + time, j);

                // migration contribution
                intensity += (-mm.getBackwardRate(j, j)) * lineages * time;
            }

            _totalIntegratedRate += intensity;

            //System.out.print(" LL: interv="+i+"\trate="+df.format(exitRate)+"\tt="+df.format(time)+"\tintrate="+ df.format(time*exitRate) + "\tsurv="+df.format(survivalProbability));

            double pointDensity = 0.0;
            Event event = intervals.getEvent(i);

            // update current time
            currentTime += time;
            //currentTime = event.time;

            switch (event.getType()) {

                case COALESCENT: {

                    if (!(event.getAboveColour() == event.getBelowColour())) {
                        throw new Error("Coalescent event changes colour");
                    }
                    // old version:
                    //pointDensity = 1.0 / N[event.aboveColour];

                    // Make sure that rounding errors do not influence the population size (relevant for
                    // non-continuous population models, e.g. piecewise constant models for the Bayesian skyline)
                    pointDensity = 1.0 / mp.getDemographic(currentTime - tinyTime, event.getAboveColour());

                    //System.out.println(" Coalescent; density="+pointDensity);

                }
                break;

                case MIGRATION: {

                    if (!(event.getAboveColour() != event.getBelowColour())) {
                        throw new Error("Colour-change event fails to change colour");
                    }
                    pointDensity = mm.getBackwardRate(event.getBelowColour(), event.getAboveColour());

                    //System.out.println(" Migration; density="+pointDensity);

                }
                break;
                case SAMPLE: {

                    pointDensity = 1.0;
                    //System.out.println(" Sample");
                }
                break;
            }

            logL += Math.log(pointDensity) - intensity;
        }

        /*
        System.out.println("Full likelihood "+logL);
        System.out.println("Integrated exit rate "+_totalIntegratedRate);
        */

        return logL;
    }

    /**
     * Calculate the (2-population) structured coalescent likelihood for the given tree,
     * tip-colors, population sizes and migration rates.
     * @param tree
     * @param initial the probability of being in population 0 at the tips
     * @param N the population sizes of the two populations
     * @param m the migration rates between the two populations
     */
    /*public double calculateLogLikelihood(Tree tree, double[] initial, double[] N, double[] m) {

        double logL = 0.0;

        // get the coalescent and add sample intervals
        TreeIntervals intervals = new TreeIntervals(tree);

        // the partial probs at all nodes.
        double[] p = new double[tree.getNodeCount()];

        // initialize the partials at all external nodes.
        // index of initial array should correspond to the index in external node list of tree.
        for (int i = 0; i < initial.length; i++) {
            p[i] = initial[i];
        }

        // the coalescence rates among all pairs of lineages
        double[][] cl = new double[tree.getNodeCount()][tree.getNodeCount()];

        // loop through intervals, updating partials
        for (int i = 0; i < intervals.getIntervalCount(); i++) {

            double ti = intervals.getInterval(i);
            List lineages = intervals.getLineages(i);

            // calculate new partials after time ti for all lineages in the current interval
            for (int j = 0; j < lineages.size(); j++) {

                NodeRef lineage = (NodeRef)lineages.get(j);
                int li = lineage.getNumber();

                // update the partial for node j
                p[li] = timeEvolution(p[li], ti);
            }

            // if this interval is a coalescent then calculate coalescent contribution to likelihood
            if (intervals.getIntervalType(i) == IntervalType.COALESCENT) {

                // calculate coalescence rates for all pairs of current lineages
                for (int j = 0; j < lineages.size(); j++) {

                    NodeRef lineage1 = (NodeRef)lineages.get(j);
                    int i1 = lineage1.getNumber();

                    for (int k = j+1; k < lineages.size(); k++) {

                        NodeRef lineage2 = (NodeRef)lineages.get(k);
                        int i2 = lineage2.getNumber();

                        double p0 = p[i1]*p[i2]; // probability both lineages in population 0
                        double p1 = (1-p[i1])*(1-p[i2]); // probability both lineages in population 1
                        double psame = p0 + p1; // probability both lineages i1 and i2 in same population
                        double pdiff = 1.0 - psame;

                        cl[i1][i2] = cl[i2][i1] = p0/N[0] + p1/N[1];
                    }
                }

                // do something with cl to calculate coalescent density

                // get nodes involved in coalescent event
                NodeRef parent = intervals.getCoalescentNode(i);
                int pi = parent.getNumber();
                NodeRef left = tree.getChild(parent,0);
                NodeRef right = tree.getChild(parent,1);
                int i1 = left.getNumber();
                int i2 = right.getNumber();

                // calculate the partial prob for the beginning of the coalesced lineage
                double p0 = p[i1]*p[i2]; // probability both lineages in population 0
                double p1 = (1-p[i1])*(1-p[i2]); // probability both lineages in population 1
                double psame = p0 + p1; // probability both lineages in same population

                p[pi] = p0 / psame; // re-normalized probability coalescent event in population 0

                // add the likelihood contribution from co-location probability psame
                // need to account for different coalescent rates here as well!
                logL += Math.log(psame);

            } else {
                // interval ended in a sample event -- nothing left to do
            }

            // at this point all p values have been updated for the start of the next coalescent interval.
        }

        return logL;
    } */

    /*
    *//**
     * @return the probability of being in population 0 after time t, starting from prob p
     *//*
    private double timeEvolution(double p, double t) {
        throw new RuntimeException("Not implemented");
    }*/
}
