/*
 * Copyright 2010 Lari Natri <lari.natri@gmail.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program 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 General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package compid.report.protgroup;

import compid.data.tuples.ProteinHit.GroupMemberStatus;
import compid.report.protgroup.ProtGroupParams.LinkageMethod;
import net.aavalla.utils.jobpool.parameters.SyncEnumSet;
import net.aavalla.utils.general.Pair;
import net.aavalla.utils.general.UniqueComparable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.TreeMap;

/**
 * A collection of protein groups. Thread-safe.
 *
 * @author Lari Natri
 */
public final class ProtGroupCollection implements Iterable<ProtGroup> {
    

    private final LinkedHashSet<ProtGroup> groups;
    private final List<ProtGroupParams> clusterLog;

    /**
     * Constructs an empty collection. Add groups using add(ProtGroup) method.
     */
    public ProtGroupCollection() {
        this.groups = new LinkedHashSet<ProtGroup>(256);
        this.clusterLog = new ArrayList<ProtGroupParams>(1);
    }


    /**
     * Adds a protein group to this collection. If the group isn't marked
     * complete, it will be before this method exits and additional group
     * members cannot be added anymore.
     * @param group complete protein group to add
     * @return true if group was actually added, false if it existed beforehand
     */
    public synchronized final boolean add(ProtGroup group)
            throws IllegalStateException
    {
        if (!group.isComplete()) {
            group.setComplete();
        }
        return this.groups.add(group);
    }

    /**
     * Returns the amount of protein groups in this collection at this time.
     * @return size of the collection
     */
    public synchronized final int size() {
        return this.groups.size();
    }

    @Override
    public synchronized Iterator<ProtGroup> iterator() {
        return this.groups.iterator();
    }



    /**
     * Returns a copy of clustering log. It contains (in execution order) all
     * of the clusterings made to this protein group collection until this time.
     * @return a list of clustering operations performed
     */
    public synchronized final List<ProtGroupParams> getClusteringLog() {
        return new ArrayList<ProtGroupParams>(this.clusterLog);
    }

    /**
     * Tries to combine the groups in this collection as per requested by the
     * given parameters.
     * @param p parameters for clustering
     */
    public synchronized final void clusterize(ProtGroupParams p) {

        ProteinPairScorer protPairScorer
                = new ProteinPairScorer(p.useAccession,
                                        p.method,
                                        p.strictPepEquality,
                                        p.minPepsForPeptideBasedMethods,
                                        p.seqMaxSubsts,
                                        true);

        // Contains scores for each group pair that can be combined
        TreeMap<UniqueComparable<Double>,Pair<ProtGroup>> scores =
                new TreeMap<UniqueComparable<Double>,Pair<ProtGroup>>();

        // Contains mapping from protein group to all its scores
        HashMap<ProtGroup,LinkedList<UniqueComparable<Double>>> groupToScores =
                new HashMap<ProtGroup,LinkedList<UniqueComparable<Double>>>();

        // Get scores for each pair in the original groups

        System.out.println("DEBUG: ProtGroupCollection: Scoring original pairs...");

        ArrayList<ProtGroup> l = new ArrayList<ProtGroup>(this.groups);
        final int origGroupCount = l.size();
        for (int i=0; i<origGroupCount; ++i) {
            ProtGroup g1 = l.get(i);
            LinkedList<UniqueComparable<Double>> g1Scores;
            if (i==0) {
                g1Scores = new LinkedList<UniqueComparable<Double>>();
                groupToScores.put(g1, g1Scores);
            } else {
                g1Scores = groupToScores.get(g1);
            }

            for (int j=i+1; j<l.size(); ++j) {
                ProtGroup g2 = l.get(j);
                LinkedList<UniqueComparable<Double>> g2Scores;
                if (i==0) {
                    g2Scores = new LinkedList<UniqueComparable<Double>>();
                    groupToScores.put(g2, g2Scores);
                } else {
                    g2Scores = groupToScores.get(g2);
                }
                Double score = this.getGroupCombineScore(g1, g2, p.groupThLinkage, p.cutoffMinPeps, p.cutoffStatuses1, p.cutoffStatuses2, protPairScorer);
                if (score!=null) { // can be combined
                    UniqueComparable<Double> uniqueScore = new UniqueComparable<Double>(score);
                    g1Scores.add(uniqueScore);
                    g2Scores.add(uniqueScore);
                    scores.put(uniqueScore, new Pair<ProtGroup>(g1, g2));
                }
            }
        }
        l = null; // free memory

        System.out.println("DEBUG: ProtGroupCollection: Clustering...");
        int debug_passes = 0;
        
        while(!scores.isEmpty()) { // loop while there are groups to combine

            ++debug_passes;

            Entry<UniqueComparable<Double>,Pair<ProtGroup>> bestEntry 
                    = scores.pollLastEntry(); // gets and removes best scoring entry
            ProtGroup g1 = bestEntry.getValue().o1;
            ProtGroup g2 = bestEntry.getValue().o2;
            for (UniqueComparable<Double> oldUniqueScore : groupToScores.get(g1)) {
                scores.remove(oldUniqueScore);
            }
            for (UniqueComparable<Double> oldUniqueScore : groupToScores.get(g2)) {
                scores.remove(oldUniqueScore);
            }
            groupToScores.remove(g1);
            groupToScores.remove(g2);
            this.groups.remove(g1);
            this.groups.remove(g2);

            ProtGroup newG = new ProtGroup(g1,g2);

            LinkedList<UniqueComparable<Double>> newGScores = new LinkedList<UniqueComparable<Double>>();
            groupToScores.put(newG, newGScores);
            for (ProtGroup g : this.groups) {
                LinkedList<UniqueComparable<Double>> oldGScores = groupToScores.get(g);
                Double score = this.getGroupCombineScore(newG, g, p.groupThLinkage, p.cutoffMinPeps, p.cutoffStatuses1, p.cutoffStatuses2, protPairScorer);
                if (score!=null) {
                    UniqueComparable<Double> uniqueScore = new UniqueComparable<Double>(score);
                    oldGScores.add(uniqueScore);
                    newGScores.add(uniqueScore);
                    scores.put(uniqueScore, new Pair<ProtGroup>(newG, g));
                }
            }
            this.groups.add(newG);

        }

        System.out.println("DEBUG: ProtGroupCollection: clustering passes: "+debug_passes);
        System.out.println("DEBUG: ProtGroupCollection: cache hits: "+protPairScorer.cacheHits() + ", misses: "+protPairScorer.cacheMisses());

        this.clusterLog.add(p);
        // result is already in this.groups
    }

    private Double getGroupCombineScore(ProtGroup g1, ProtGroup g2, LinkageMethod lm, int thMinPeps, SyncEnumSet<GroupMemberStatus> thStat1, SyncEnumSet<GroupMemberStatus> thStat2, ProteinPairScorer pps) {
        double scoreSum = 0;
        double bestScore = Double.NEGATIVE_INFINITY;
        int pairsPassingThreshold = 0;
        int pairsHavingScore = 0;

        for (ProtGroupMember p1 : g1.getMembers()) {

            for (ProtGroupMember p2 : g2.getMembers()) {


                boolean passesTh = true; // will be set to false if fails threshold test

                // Check minimum peptide thresholds
                if (thMinPeps>1) {
                    if (p1.hasQueries() && p1.getDistinctPeptideCount() < thMinPeps) {
                        passesTh = false;
                    } else if (p2.hasQueries() && p2.getDistinctPeptideCount() < thMinPeps) {
                        passesTh = false;
                    }
                }

                // Check original group statuses threshold
                if (thStat1.contains(p1.getType()) && thStat2.contains(p2.getType())) {
                    // passes
                } else if (thStat1.contains(p2.getType()) && thStat2.contains(p1.getType())) {
                    // passes
                } else {
                    passesTh = false;
                }


                if (passesTh) {
                    ++pairsPassingThreshold;
                } else {
                    continue;
                }


                Double score = pps.similarity(p1, p2);
                if (score==null) {
                    if (lm.equals(LinkageMethod.Complete)) {
                        return null;
                    }
                } else {
                    ++pairsHavingScore;
                    scoreSum+=score;
                    if (score>bestScore) {
                        bestScore = score;
                    }
                }
            } // inner for
        } // outer for

        if (pairsHavingScore > 0) {
            switch(lm) {
                case Complete :
                    if (pairsPassingThreshold==pairsHavingScore) {
                        return scoreSum;
                    } else {
                        return null;
                    }
                case Single :
                    if (pairsHavingScore > 0) {
                        return bestScore;
                    } else {
                        return null;
                    }
                default :
                    throw new RuntimeException("Programming error: new linkage " +
                            "method added but not implemented");
            }
        } else {
            return null;
        }
    }

}
