package org.baderlab.brain;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collection;
import java.util.Iterator;

/**
 * Created by IntelliJ IDEA.
 * User: moyez
 * Date: Oct 21, 2007
 * Time: 6:56:34 PM
 * To change this template use File | Settings | File Templates.
 */
public class ProfileAlignment extends AvgLinkHierarchicalClustering {

    private ArrayList profileList = null;
    private ArrayList alignedProfileList = null;
    private ArrayList alignmentPairs = null;
    private ArrayList alignmentPairsCopy = null;
    private AlignmentMatrix alignmentMatrix = null;
    private HashMap alignmentTarget = null;
    private ArrayList alignmentOffset = null;

    //testing only
    private ArrayList alignmentResult = null;
    //private int[][] result = null;
    private HashMap alignmentResultIndex = null;

    private HashMap profileAlignmentPositionMap = null;


    public ProfileAlignment(ArrayList profileList, AlignmentMatrix alignmentMatrix) {
        super(alignmentMatrix);
        this.profileList = profileList;
        this.alignmentMatrix = alignmentMatrix;

        //initialize results
        alignedProfileList = new ArrayList();
        alignmentPairs = new ArrayList();
        alignmentPairsCopy = new ArrayList();
        alignmentTarget = new HashMap<Integer, Integer>();
        alignmentOffset = new ArrayList();

        //testing only
        alignmentResult = new ArrayList<String>();             //list of strings, each being an aligned profile
        alignmentResultIndex = new HashMap<Integer, Integer>();//key=profileList index; value=alignmentResult index

        //stores the absolute alignment position of each profile
        profileAlignmentPositionMap = new HashMap<String, Integer>();
    }

    /**
     * Private class to hold a pair of integers for distance matrix lookup
     */
    private class Pair {
        public int i, j;

        public Pair() {
            this.i = 0;
            this.j = 0;
        }

        //different instances of a Pair object are equivalent if they contain the same indexes in any order
        //overriding 'equals' which is used by the ArrayList.contains method called in 'isUsedPair()'
        public boolean equals(Object arg) {
            if ((arg != null) && (arg instanceof Pair)) {
                Pair pair = (Pair) arg;
                if ((pair.i == i) && (pair.j == j))
                    return true;
                else if ((pair.j == i) && (pair.i == j))
                    return true;
            }
            return false;
        }
    }

    /**
     * Main method that runs the alignment
     */
    @Override
    public void run() {
        buildClosestPairList();
        normalizePositionMapToZero();
        Pair pair = (Pair)alignmentPairs.get(0);
        alignmentPairsCopy.remove(0);
        align(pair.i, pair.j);

        //debug -- print the aligned profiles
//        System.out.println("\nAligned Profiles:\n");
//        for (int i = 0; i < alignedProfileList.size(); i++) {
//            ProteinProfile profile = (ProteinProfile)alignedProfileList.get(i);
//            System.out.println(profile.getName() + "\n");
//        }
//


//        System.out.println("\nAlignment Result:\n");
//        for (int i = 0; i < alignmentResult.size(); i++) {
//            System.out.println(alignmentResult.get(i) + "\n");
//        }


        displayAbsoluteAlignmentPositions();


    }


    /**
     * Add the pair of profiles to the absolute alignment position map.
     * Only add a profile if it has not been previously mapped.
     *
     * NOTE: it is assumed that either NONE or ONLY ONE of the profiles may already be in the map;
     *   both profiles cannot already be mapped as this would mean this method has been called before
     *   with the exact same pair argument (which should not happen)
     *
     * @param pair
     */

    private void addPairToPositionMap(Pair pair) {

        ProteinProfile profile1 = (ProteinProfile) profileList.get(pair.i);
        ProteinProfile profile2 = (ProteinProfile) profileList.get(pair.j);

        System.out.println("-------------------------------------------------------------------------------------------------------------------------");
        System.out.println("Placing Pair: " + profile1.getName() + ", " + profile2.getName());

        //if both profiles have already been positioned, do nothing
        if (profileAlignmentPositionMap.containsKey(profile1.getName()) && profileAlignmentPositionMap.containsKey(profile2.getName())) {

            System.out.println("Both profiles have already been positioned. Continuing.");
            return;
        }

        //if neither profiles have been positioned, add them both and offset the 2nd profile relative to the first
        if (!profileAlignmentPositionMap.containsKey(profile1.getName()) && !profileAlignmentPositionMap.containsKey(profile2.getName())) {
            int offset = alignmentMatrix.getAlignmentValue(pair.i, pair.j);
            profileAlignmentPositionMap.put(profile1.getName(), 0);
            profileAlignmentPositionMap.put(profile2.getName(), offset);

            System.out.println("Neither profile has been positioned.");
            System.out.println("Inserting " + profile1.getName() + " at position 0");
            System.out.println("Inserting " + profile2.getName() + " at position " + offset);

            return;

        }

        //if profile not positioned, add it to the map at position 0
        if (profileAlignmentPositionMap.containsKey(profile1.getName())) {
            int position = (Integer) profileAlignmentPositionMap.get(profile1.getName());
            int offset = alignmentMatrix.getAlignmentValue(pair.i, pair.j);
            profileAlignmentPositionMap.put(profile2.getName(), position + offset);

            System.out.println(profile1.getName() + " is already positioned at positiion " + position);
            System.out.println(profile2.getName() + " is not positioned.");
            System.out.println(profile2.getName() + " is offset by " + offset + " relative to " + profile1.getName());
            System.out.println("Inserting " + profile2.getName() + "at position " + (position + offset));

            return;
        }

        if (profileAlignmentPositionMap.containsKey(profile2.getName())) {
            int position = (Integer) profileAlignmentPositionMap.get(profile2.getName());
            int offset = alignmentMatrix.getAlignmentValue(pair.j, pair.i);
            profileAlignmentPositionMap.put(profile1.getName(), position + offset);

            System.out.println(profile2.getName() + " is already positioned at positiion " + position);
            System.out.println(profile1.getName() + " is not positioned.");
            System.out.println(profile1.getName() + " is offset by " + offset + " relative to " + profile2.getName());
            System.out.println("Inserting " + profile1.getName() + "at position " + (position + offset));

            return;
        }
    }

    /**
     * Shifts absolute alignment positions "to the right" so that there are no negative position values
     * and the "leftmost" or minimum alignment position is 0. This is necessary for later drawing of the logo images.
     */
    private void normalizePositionMapToZero() {

        //debug
        System.out.println ("Adjusting absolute positions...");

        //determine minimum (left most) position
        int minPosition = Integer.MAX_VALUE;
        for (int i = 0; i < alignedProfileList.size(); i++) {
            String name = ((ProteinProfile)alignedProfileList.get(i)).getName();
            minPosition = Math.min(minPosition, (Integer) profileAlignmentPositionMap.get(name));
        }

        //debug
        System.out.println ("Minimum Position: " + minPosition);

        //shift all positions "to the right" so that minimum position is zero
        for (int i = 0; i < alignedProfileList.size(); i++) {
            String name = ((ProteinProfile)alignedProfileList.get(i)).getName();
            int position = (Integer) profileAlignmentPositionMap.get(name);
            profileAlignmentPositionMap.put(name, position - minPosition);
        }

        //testing
        //determine minimum (left most) position
        minPosition = Integer.MAX_VALUE;
        for (int i = 0; i < alignedProfileList.size(); i++) {
            String name = ((ProteinProfile)alignedProfileList.get(i)).getName();
            minPosition = Math.min(minPosition, (Integer) profileAlignmentPositionMap.get(name));
        }
        System.out.println ("New Minimum Position: " + minPosition + "\n");
        

    }


    public void displayAbsoluteAlignmentPositions() {

        for (int i = 0; i < alignedProfileList.size(); i++) {
            String name = ((ProteinProfile)alignedProfileList.get(i)).getName();
            System.out.println(name + "\t" + profileAlignmentPositionMap.get(name));
        }
    }


    public int getAbsoluteAlignmentPosition(ProteinProfile profile) {
        return (Integer) profileAlignmentPositionMap.get(profile.getName());
    }



    //Returns True iff the given pair and its reverse pair have not been flagged as 'used'
    private boolean isUsedPair(Pair pair) {
        if (alignmentPairs.contains(pair))
            return true;
        return false;
    }


    /*
    Aligns the profiles represented in 'distanceMatrix'
     */
    private void buildClosestPairList() {

        //base case - stop when all profiles have been represented
        if (alignedProfileList.size() == profileList.size())
            return;

        //inductive case
        Pair closestPair = findClosestPair();
        if (closestPair == null) {
            throw new IllegalStateException(this.getClass().getName() +  ": Closest pair not found in distance matrix");
        }
        addPair(closestPair);
        buildClosestPairList();

    }

    
    /**
     * This function searches the alignment matrix to find the pair with the shortest
     * distance between them.
     *
     * @return The pair with the shortest distance that has not been "used" previously
     */
    private Pair findClosestPair() {
        Pair result = new Pair();
        Pair thisPair = new Pair();
        double distance = Double.MAX_VALUE;
        boolean found = false;
        for (int i = 0; i < alignmentMatrix.getMatrixDimension(); i++) {
            for (int j = 0; j < i; j++) {
                thisPair.i = i;
                thisPair.j = j;
                //if this pair hasn't been used, consider it
                if ((isUsedPair(thisPair) == false) && (alignmentMatrix.getValue(i, j) < distance)) {
                    distance = alignmentMatrix.getValue(i, j);
                    result.i = i;
                    result.j = j;
                    found = true;
                }
            }
        }
        if (found == true)
            return result;
        return null;
    }

    //Creates a new alignment for the given pair
    private void addPair(Pair pair) {
        if (pair == null) {
            System.out.println(this.getClass().getName() +  ": Null argument provided.");
            throw new IllegalArgumentException();
        }
        addPairToAlignedProfileList(pair);
        addPairToPositionMap(pair);
        alignmentPairs.add(pair);

        alignmentPairsCopy.add(pair);
        alignmentOffset.add(alignmentMatrix.getAlignmentValue(pair.i, pair.j));

        //TODO - get rid of this hash if not used
        alignmentTarget.put((Integer) pair.i,  (Integer) pair.j);


    }

    //Adds the profiles in a Pair to the aligned profile list and ensures that this list contains unique entries only
    private void addPairToAlignedProfileList(Pair pair) {
        ProteinProfile profile = (ProteinProfile) profileList.get(pair.i);
        if (!alignedProfileList.contains(profile)) {
             alignedProfileList.add(profile);
        }
        profile = (ProteinProfile) profileList.get(pair.j);
        if (!alignedProfileList.contains(profile)) {
             alignedProfileList.add(profile);
        }
    }

    /**
     * Following methods are for building the alignment result after closest pair list has been determined
     */
    private void align(int a, int b) {

        if (b == -1)
            return;

        addPairToAlignment(a, b);
        align(a, relatedProfile(a));
        align(b, relatedProfile(b));
    }



    //two profiles are related if they are paired
    //this method searches the alignment pair list and returns the index of the first related profile found, if any
    //if no sibling profile is found, returns -1
    private int relatedProfile(int index) {
        Pair pair = new Pair();
        boolean found = false;
        int i;

        //search for a related profile
        for (i = 0; i < alignmentPairsCopy.size(); i++) {
            pair = (Pair) alignmentPairsCopy.get(i);
            if ((pair.i == index) || (pair.j == index)) {
                found = true;
                break;
            }
        }
        //if we find a related pair, return the related profile index
        if (found) {
            int iFound = pair.i;
            int jFound = pair.j;
            alignmentPairsCopy.remove(i);

            if (pair.i == index) {
                return pair.j;
            }
            if (pair.j == index) {
                return pair.i;
            }
        }
        return -1;
    }

    /**
     *
     * @param a
     * @param b
     */
    private void addPairToAlignment(int a, int b) {
        if (!alignmentResultIndex.containsKey(a)) {
            alignmentResult.add(getAlignmentString(a, -1));
            alignmentResultIndex.put(a, alignmentResult.indexOf(a));
        }
        alignmentResult.add(getAlignmentString(b, a));
        alignmentResultIndex.put(b, alignmentResult.indexOf(b));
    }

    private String getAlignmentString(int sourceIndex, int targetIndex) {
        StringBuffer sb = new StringBuffer();
        String lineSep = System.getProperty("line.separator");

        ProteinProfile profile = (ProteinProfile) profileList.get(sourceIndex);
        sb.append(profile.getName());

        if (targetIndex != -1) {
            ProteinProfile targetProfile = (ProteinProfile) profileList.get(targetIndex);
            sb.append("\t" + targetProfile.getName());
            sb.append("\t" + alignmentMatrix.getAlignmentValue(sourceIndex, targetIndex));
        }

        return sb.toString();
    }


    /**
     * Return a copy of the alignment matrix.
     * @return AlignmentMatrix object
     */
    public AlignmentMatrix getAlignmentMatrixCopy() {
        return alignmentMatrix.copyAlignment();
    }




    public String toString() {
        StringBuffer sb = new StringBuffer();
        String lineSep = System.getProperty("line.separator");

        for (int i = 0; i < alignmentPairs.size(); i++) {
            Pair pair = (Pair) alignmentPairs.get(i);
            ProteinProfile profileI = (ProteinProfile) profileList.get(pair.i);
            ProteinProfile profileJ = (ProteinProfile) profileList.get(pair.j);
            sb.append(profileI.getName() + "\t" + profileJ.getName() );
//            sb.append(profileI.getName() + "[" + pair.i + "]" + "\t" + profileJ.getName() + "[" + pair.j + "]");
            sb.append(lineSep);
        }
        sb.append(lineSep);

        return (sb.toString());
    }

}
