/*
 * Created on May 12, 2005
 *
 */
package com.kellegous.mas864.clusterss;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import Jama.CholeskyDecomposition;
import Jama.EigenvalueDecomposition;
import Jama.Matrix;

/**
 * @author knorton
 * Copyright (C) 2005 MIT Media Lab, All Right Reserved.
 */
public class ClusteringMachine {
    private KernelGenerator 	generator;
    private ArrayList			listeners;
    
    public ClusteringMachine(KernelGenerator generator) {
        this.generator = generator;
        this.listeners = new ArrayList();
    }
    
    public void addClusteringMachineListener(ClusteringMachineListener listener) {
    		this.listeners.add(listener);
    }
    
    public void removeClusteringMachineListener(ClusteringMachineListener listener) {
    		this.listeners.remove(listener);
    }
    
    private void dispatchStartedEvent() {
    		ClusteringMachineEvent e = new ClusteringMachineEvent(this);
    		for (Iterator i=this.listeners.iterator();i.hasNext();)
    			((ClusteringMachineListener)i.next()).clusterProcessStarted(e);
    }
    
    private void dispatchCompletedEvent() {
    		ClusteringMachineEvent e = new ClusteringMachineEvent(this);
    		for (Iterator i=this.listeners.iterator();i.hasNext();)
    			((ClusteringMachineListener)i.next()).clusterProcessCompleted(e);
    }
    
    private void dispatchStatusChangedEvent(String message) {
    		ClusteringMachineEvent e = new ClusteringMachineEvent(this,message);
    		for (Iterator i=this.listeners.iterator();i.hasNext();)
    			((ClusteringMachineListener)i.next()).clusterStatusChanged(e);
    }
    
    public void dispatchKernelGeneratedEvent(Matrix kernel) {
        ClusteringMachineEvent e = new ClusteringMachineEvent(this,kernel);
        for (Iterator i=this.listeners.iterator();i.hasNext();)
            ((ClusteringMachineListener)i.next()).clusterKernelGenerated(e);
    }
    
    private Matrix generateDiagonalMatrix(Matrix k) {
        int n = k.getColumnDimension();
        Matrix d = new Matrix(
                k.getColumnDimension(),
                k.getRowDimension());
        for (int i=0;i<n;i++) {
            double s = 0.0;
            for (int j=0;j<n;j++)
                s+= k.get(i,j);
            d.set(i,i,s);
        }
        return d;
    }
    

    
    private double aff(List a, List b) {
        double sum = 0.0;
        for (Iterator ai=a.iterator();ai.hasNext();) {
            int an = ((Integer)ai.next()).intValue();
            for (Iterator bi=b.iterator();bi.hasNext();) {
                int bn = ((Integer)bi.next()).intValue();
                sum += this.generator.getWeight(an,bn);
            }
        }
        return sum;
    }
    
    private double ncut(List a, List b, List v) {
        if (a.size()==0 || b.size()==0 || v.size()==0)
            return Double.MAX_VALUE;
        double x = aff(a,b);
        return (x/aff(a,v) + x/aff(b,v));
    }
    
    public Cluster[] split(Cluster all) {
        Cluster[] out = new Cluster[] {
                new Cluster(),
                new Cluster()
        };
        
        if (all.size()<=1) {
            return out;
        }
        
        Matrix wm = new Matrix(all.size(),all.size());
        for (int i=0;i<all.size();i++) {
            int ii = ((Integer)all.get(i)).intValue();
            for (int j=0;j<all.size();j++) {
                int ji = ((Integer)all.get(j)).intValue();
                wm.set(i,j,this.generator.getWeight(ii,ji));
            }
        }
        
        Matrix dm = new Matrix(all.size(),all.size());
        for (int i=0;i<all.size();i++) {
            double sum = 0;
            for (int j=0;j<all.size();j++) {
                sum += wm.get(i,j);
            }
            dm.set(i,i,sum);
        }
        Matrix fm = dm.minus(wm);
        
        CholeskyDecomposition chd = dm.chol();
        Matrix rm = chd.getL();
        
        Matrix hm = rm.transpose().solveTranspose(fm).transpose();
        Matrix em = rm.solve(hm);
        
        //do this only on first pass.
        if (all.size()==this.generator.getSize())
            this.dispatchKernelGeneratedEvent(em.copy());
        
        EigenvalueDecomposition evd = em.eig();
        double[] eigvals = evd.getRealEigenvalues();
        Matrix   eigvecs = evd.getV();
        
        double mineigval = eigvals[0];
        int    mineigidx = 0;
        for (int i=1;i<eigvals.length;i++) {
            if (eigvals[i]<mineigval) {
                mineigval = eigvals[i];
                mineigidx = i;
            }
        }
        
        Matrix vector = rm.transpose().inverse().times(
                eigvecs.getMatrix(
                        0,
                        eigvecs.getRowDimension()-1,
                        mineigidx,
                        mineigidx));
        
        double maxE=vector.get(0,0), minE=vector.get(0,0);
        for (int i=1;i<vector.getRowDimension();i++) {
            if (maxE<vector.get(i,0))
                maxE = vector.get(i,0);
            if (minE>vector.get(i,0))
                minE = vector.get(i,0);
        }
        
        double midE = (maxE+minE)/2.0;
        
        for (int i=0;i<vector.getRowDimension();i++) {
            if (vector.get(i,0)<=midE)
                out[0].add(all.get(i));
            else                
                out[1].add(all.get(i));
        }
        
        return out;
    }
    
    public Cluster[] cluster() {
        this.dispatchStartedEvent();
        Cluster set = this.generateFullSetList(
                this.generator.getSize());
        ArrayList done = new ArrayList();
        ArrayList pend = new ArrayList();
        pend.add(set);
        int count = 1;
        
        double m = Double.MIN_VALUE;
        while (!pend.isEmpty()) {
            this.dispatchStatusChangedEvent("Cluster: Iteration #"+(count++)+", NCut="+m);            
            Cluster 		p = (Cluster)pend.remove(0);
            Cluster[] 	c = this.split(p);
            m=this.ncut(c[0],c[1],p);
            
            if (m>0) {
                done.add(p);
            }
            else {
                pend.add(c[0]);
                pend.add(c[1]);
            }
        }
		Cluster[] out = new Cluster[done.size()];
		done.toArray(out);
		this.dispatchCompletedEvent();
		return out;        
    }
    
    private Cluster generateFullSetList(int n) {
        Cluster c = new Cluster();
        for (int i=0;i<n;i++)
            c.add(new Integer(i));
        return c;
    }    
}
