/*
 * CountConstrainedRankedHistories.java
 *
 * Copyright (C) 2002-2011 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.tree;

import dr.math.Binomial;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;

/**
 * @author Alexei Drummond
 */
public class CountConstrainedRankedHistories {


    private static BitSet createClade(String s) {
        BitSet bitSet = new BitSet();
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '1') {
                bitSet.set(i);
            }
        }
        return bitSet;
    }

    /**
     * Compute the number of 'free' nodes under each calibration
     *
     * @param constraints
     * @param f
     */
    private static void computeF(BitSet[] constraints, int[] f) {

        for (int i = 0; i < f.length; i++) {

            BitSet set = new BitSet();
            set.or(constraints[i]);

            int subCladeCount = 0;

            for (int j = i + 1; j < f.length; j++) {

                int parent = parent(constraints, j);

                if (parent == i) {
                    subCladeCount += 1;
                    set.andNot(constraints[j]);
                }
            }
            f[i] = set.cardinality() + subCladeCount - 2;
        }
    }

    private static long Nc(int[] f) {

        int C = f.length;
        long Nc = 1;
        for (int i = 0; i < f.length; i++) {
            if (f[i] != 0) Nc *= Binomial.choose(f[i] + C - i - 1, C - i - 1);
        }
        return Nc;
    }

    // returns the number of ranked forests with n tips and k internal nodes

    private static long R(int n, int k) {
        long Rnk = 1;
        for (int i = 0; i < k; i++) {
            Rnk *= Binomial.choose2(n - i);
        }

        if (Rnk == 0) {
            //System.out.println("R(" + n + ", " + k + ") = " + 0);

        }

        return Rnk;
    }

    private static long X(int[][] n, int[][] k, int[] f) {

        int C = f.length;

        long X = 1;

        // for all levels
        for (int i = 0; i < C; i++) {
            // for all forests in level i
            for (int j = 0; j < i + 1; j++) {

//                if (n[i][j] == 1 && k[i][j] == 1) {
//                    System.out.println("n(" + i + ", " + j + ") == 1 && k(" + i + ", " + j + ") == 1");
//                }

                X *= R(n[i][j], k[i][j]);
            }
        }

        // for all levels
        for (int i = 0; i < C; i++) {
            X *= totalOrder(k[i]);
        }
        return X;
    }

    private static int[] nC(BitSet[] clades) {

        int[] nC = new int[clades.length];

        for (int i = 0; i < clades.length; i++) {
            nC[i] = clades[i].cardinality();

            BitSet set = new BitSet();
            set.or(clades[i]);


            for (int j = i + 1; j < clades.length; j++) {
                if (clades[j].intersects(clades[i])) {
                    set.andNot(clades[j]);
                }
            }
            nC[i] = set.cardinality();
        }
        return nC;
    }


    // return the number of total orders of k.length partial orders of lengths k[0], k[1], ..., k[k.length-1]

    private static long totalOrder(int[] k) {

        long T = 1;

        int m = k[0];
        int n = 0;
        for (int i = 0; i < k.length - 1; i++) {
            m += k[i + 1];
            n += k[i];
            T *= Binomial.choose(m, n);
        }
        return T;
    }

    private static int parent(BitSet[] clades, int child) {

        for (int parent = child - 1; parent >= 0; parent--) {
            if (clades[parent].intersects(clades[child])) {
                return parent;
            }
        }
        return 0;
    }

    private static void computeN(int[][] k, int[][] n, int[] nC, BitSet[] clades) {

        int C = nC.length;

        for (int i = 0; i < C; i++) {
            n[C - 1][i] = nC[i];
        }

        for (int level = C - 1; level > 0; level--) {
            for (int clade = 0; clade < level; clade++) {
                n[level - 1][clade] = n[level][clade] - k[level][clade];

            }
        }

        for (int level = C - 1; level > 0; level--) {
            int parent = parent(clades, level);

            //System.out.println("parent = " + parent);

            for (int c = level - 1; c >= parent; c--) {
                n[c][parent] += 1;
            }
        }
    }

    private static boolean kton_next(int[][] v, int[] n, int level) {

        int k = v[level].length - 1;

        if (k < 0) {
            if (level < v.length-1) {

                reset(v, level);

                return kton_next(v, n, level + 1);
                //return true;
            } else return false;
        }

        //System.out.println("level = " + level + ", k = " + k);

        while (v[level][k] == n[level] - 1) {
            k -= 1;
            if (k < 0) {
                // reset and advance 1 at the next level up, or if at top level then end

                if (level < v.length-1) {

                    reset(v, level);
                    return kton_next(v, n, level + 1);
                    //return true;
                } else return false;
            }
        }
        int vk = v[level][k] + 1;

        while (k < v[level].length) {
            v[level][k] = vk;
            k += 1;
        }
        return true;
    }

    private static void reset(int[][] v, int level) {
        for (int l = 0; l < level; l++) {
            for (int i = 0; i < v[l].length; i++) {
                v[l][i] = 0;
            }
        }
    }

    public static void print(int[][] k, int[][] n) {

        System.out.println("k:");
        for (int level = 0; level < k.length; level++) {
            for (int sublevel = 0; sublevel < k[level].length; sublevel++) {

                int subforest = sublevel + level;

                System.out.print(k[level][sublevel] + "\t");

            }
            System.out.println();
        }

        System.out.println("n:");
        for (int level = 0; level < k.length; level++) {
            for (int sublevel = 0; sublevel < k[level].length; sublevel++) {
                System.out.print(n[level][sublevel] + "\t");

            }
            System.out.println();
        }

    }


    public static void main(String[] args) {

        //String[] constraintStrings = {"1111111", "1110000", "0000011"};

        String[] constraintStrings = {
                "11111111111111111111",
                "00000000011110000000",
                "00000000000110000000",
                "00000000011000000000",
                "00000000000000011111",
                "11111111100000000000",
                "11111111000000000000",
                "11111000000000000000"};
//
//        String[] constraintStrings = {
//                "111111111",
//                "111111110",
//                "111110000"};


        // number of constraints, including the "1,1,...,1,1" constraint
        int C = constraintStrings.length;

        System.out.println("n = " + constraintStrings[0].length());
        System.out.println("#calibrations = " + (constraintStrings.length - 1));

        System.out.println("Constraints:");
        for (int i = 0; i < C; i++) {
            System.out.println("  " + constraintStrings[i]);
        }

        BitSet[] constraints = new BitSet[C];

        for (int i = 0; i < C; i++) {
            constraints[i] = createClade(constraintStrings[i]);
        }

        int[] f = new int[C];

        computeF(constraints, f);

//        for (int i = 0; i < f.length; i++) {
//            System.out.println((i + 1) + " : " + f[i]);
//        }

        //System.out.println("Nc = " + Nc(f));

        int[] nC = nC(constraints);
//        for (int i = 0; i < nC.length; i++) {
//            System.out.println("n_{" + (C - 1) + "," + i + "} = " + nC[i]);
//        }


        //System.out.println("T(1,2,1) = " + totalOrder(new int[]{1, 2, 1}));

        int[][] k = new int[C][];
        int[][] n = new int[C][];
        for (int i = 0; i < k.length; i++) {
            k[i] = new int[i + 1];
            n[i] = new int[i + 1];
        }

        computeN(k, n, nC, constraints);

//        for (int i = 0; i < n.length; i++) {
//            for (int j = 0; j < n[i].length; j++) {
//                System.out.println("n(" + i + "," + j + ") = " + n[i][j]);
//            }
//
//        }


        long X = 0;
        // sum X over all k orders

        int[][] korders = new int[C][];
        int[] size = new int[C];

        for (int i = 0; i < C; i++) {
            korders[i] = new int[f[i]];
            size[i] = C - i;
        }

        long startTime = System.currentTimeMillis();

        int calls = 0;
        do {
//            for (int j = 0; j < C; j++) {
//                for (int kj : korders[j]) {
//
//                    System.out.print(kj);
//                }
//                System.out.print("|");
//            }

//            System.out.println();

            for (int i = 0; i < k.length; i++) {
                for (int j = 0; j < k[i].length; j++) {
                    k[i][j] = 0;
                }
            }

            for (int j = 0; j < C; j++) {
                for (int i = 0; i < korders[j].length; i++) {

                    //System.out.println("korders[" + j + ", " + i + "] = " + korders[j][i]);
                    //System.out.println("k(" + korders[j][i] + "," + j + ") += 1");
                    //System.out.println("k.length = " + k.length);

                    k[korders[j][i] + j][j] += 1;
                }
            }

            computeN(k, n, nC, constraints);

            //print(k, n);

            //System.out.println("X = " + X(n, k, f));

            X += X(n, k, f);
            calls += 1;
        }
        while (kton_next(korders, size, 0));

        long stopTime = System.currentTimeMillis();


        System.out.println("Total # constrained ranked histories = " + X + " in " + calls + " calls.");
        System.out.println("Elapsed time " + Math.round((stopTime - startTime) / 10.0) / 100.0 + " seconds");


        //for (int i = 0; i < korders.size(); i++) {
        //
        //    k = korders.get(i);
        //
        //    computeN(k, n, nC, constraints);
        //    X += X(n, k, f);
        //
        //}
        //System.out.println(X + " combinations");


    }
}
