package org.baderlab.csplugins.brainlib;

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

/**
 * Copyright (c) 2005 Memorial Sloan-Kettering Cancer Center
 * *
 * * Code written by: Gary Bader
 * * Authors: Gary Bader, Chris Sander
 * *
 * * This library 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.1 of the License, or
 * * any later version.
 * *
 * * This library 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.  The software and
 * * documentation provided hereunder is on an "as is" basis, and
 * * Memorial Sloan-Kettering Cancer Center
 * * has no obligations to provide maintenance, support,
 * * updates, enhancements or modifications.  In no event shall the
 * * Memorial Sloan-Kettering Cancer Center
 * * be liable to any party for direct, indirect, special,
 * * incidental or consequential damages, including lost profits, arising
 * * out of the use of this software and its documentation, even if
 * * Memorial Sloan-Kettering Cancer Center
 * * has been advised of the possibility of such damage.  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 this library; if not, write to the Free Software Foundation,
 * * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 * *
 * * User: GaryBader
 * * Date: Aug 4, 2005
 * * Time: 8:26:57 PM
 */

/**
 * Implements a ragged array distance matrix for use with AvgLinkHierarchicalClustering
 * Only stores the lower triangle without the diagonal. Assumes the matrix is symmetric around the diagonal
 * and the diagonal is all zeros.
 */
public class DistanceMatrix {
    private double[] distanceMatrix = null;
    private int matrixDim = 0;
    private ArrayList labels = null;
    private double minimumDistance = Double.MAX_VALUE;
    private int mini = -1;
    private int minj = -1;

    /**
     * Initializes a distance matrix of the given dimension
     *
     * @param matrixDim The dimension of the matrix
     */
    public DistanceMatrix(int matrixDim) {
        if (matrixDim < 2) {
            throw new RuntimeException("Distance matrix size must be larger than 1");
        }
        this.matrixDim = matrixDim;
        //allocate enough space for lower triangle of the matrix minus the diagonal
        distanceMatrix = new double[(matrixDim * matrixDim - 1) / 2];
    }

    /**
     * Gets the dimension of the matrix
     *
     */
    public int getMatrixDimension() {
        return matrixDim;
    }

    /**
     * Put a value in the matrix at position (i,j)
     */
    public void setValue(int i, int j, double value) {
        if (j >= i) {
            //don't store the diagonal or upper triangle
            return;
        }
        //calculate the position in the array given the lower triangular matrix coordinates
        distanceMatrix[((i * (i - 1)) / 2) + j] = value;
    }

    /**
     * Get a value from the matrix at position (i,j)
     */
    public double getValue(int i, int j) {
        if (i == j) {
            //return 0.0 for the diagonal
            return 0.0;
        }
        if (j > i) {
            //convert to lower triangle - the matrix is assumed to be symmetric
            int oldj = j;
            j = i;
            i = oldj;
        }
        //calculate the position in the array given the lower triangular matrix coordinates
        return (distanceMatrix[((i * (i - 1)) / 2) + j]);
    }

    /**
     * Sets the labels of the elements in this matrix. The elements of the ArrayList must be
     * Strings corresponding to the elements in this matrix (in the same order)
     */
    public void setLabels(ArrayList labels) {
        this.labels = labels;
    }

    /**
     * Gets the labels for the distanceMatrix
     *
     * @return An ArrayList containing Strings in the order corresponding to the rows or columns of the distance matrix
     */
    public ArrayList getLabels() {
        return labels;
    }

    /**
     * Calculates a distance matrix given a distance metric implemented in the DistanceMetric class
     * Note: you need to extend the DistanceMetric class and override the 'calc' method for this
     * to do something useful.
     *
     * @param objectList     The list of objects to use to calculate an NxN distance matrix
     * @param distanceMetric The distance metric to use
     */
    public void calcDistances(List objectList, DistanceMetric distanceMetric) {
        //calculate the lower triangle of the distance matrix
        for (int i = 0; i < objectList.size(); i++) {
            Object object1 = (Object) objectList.get(i);
            for (int j = 0; j < i; j++) {
                Object object2 = (Object) objectList.get(j);
                double distance = distanceMetric.calc(object1, object2);
                if (distance < minimumDistance) {
                    minimumDistance = distance;
                    mini = i;
                    minj = j;
                }
                this.setValue(i, j, distance);
            }
        }
    }

    /**
     * Returns the minimum distance in the distance matrix.
     */
    public double getMinimumDistance() {
        return this.minimumDistance;
    }

    /**
     * Returns the "i" index of the matrix where (i,j) contains the minimum distance value
     */
    public int getMinimumI() {
        return this.mini;
    }

    /**
     *  Returns the "j" index of the matrix where (i,j) contains the minimum distance value
     */
    public int getMinimumJ() {
        return this.minj;
    }

    /**
     * Returns an exact copy of this DistanceMatrix object
     */
    public DistanceMatrix copy() {
        DistanceMatrix dm = new DistanceMatrix(this.matrixDim);
        System.arraycopy(this.distanceMatrix, 0, dm.distanceMatrix, 0, this.distanceMatrix.length);
        dm.setLabels(this.getLabels());
        return dm;
    }

    //TODO: normalize 0..1 distance matrix for other distance metrics - hierarchical clustering alg makes this assumption

    /**
     * Return the string representation of this matrix
     */
    public String toString() {
        StringBuffer sb = new StringBuffer();
        String lineSep = System.getProperty("line.separator");
        //header
        for (int i = 0; i < labels.size(); i++) {
            sb.append("\t" + (String) labels.get(i));
        }
        sb.append(lineSep);
        //labels rows
        for (int i = 0; i < matrixDim; i++) {
            sb.append(labels.get(i) + "\t");
            for (int j = 0; j < i; j++) {
                sb.append(this.getValue(i, j));
                if (j < i) {
                    //don't output a tab for the last one
                    sb.append("\t");
                }
            }
            sb.append(lineSep);
        }
        return (sb.toString());
    }
}
