/*
 * 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.report.protgroup.ProtGroupParams.ClusteringMethod;
import compid.sequences.Alignment;
import compid.sequences.AminoSequence;
import compid.sequences.BoundedAligner;
import net.aavalla.utils.general.Pair;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Set;

/**
 * Scores protein pairs. Thread-safe.
 * @author Lari Natri
 */
public class ProteinPairScorer {

    // The score returned when accessions are exactly equal
    private static final double EQUALACCESSIONSCORE = Double.MAX_VALUE;
    private final boolean passByAccession;
    private final ClusteringMethod method;
    private final boolean strictPep;
    private final int minPepsForCommonPeps;
    private final int maxAAsubsts;
    private final BoundedAligner aligner; // used with seq based comp
    private SoftReference<HashMap<Pair<ProtGroupMember>,Double>> softCache;
    private long cacheHits = 0;
    private long cacheMisses = 0;

    public ProteinPairScorer(final boolean passByAccession,
                             final ClusteringMethod method,
                             final boolean strictPepEquality,
                             final int minPepsForCommonPeps,
                             final int maxAAsubsts,
                             final boolean useCache)
    {

        this.passByAccession = passByAccession;
        this.method = method;
        this.strictPep = strictPepEquality;
        this.minPepsForCommonPeps = minPepsForCommonPeps;
        this.maxAAsubsts = maxAAsubsts;

        if (method.equals(ClusteringMethod.Sequence)) {
            this.aligner = new BoundedAligner(maxAAsubsts);
        } else {
            this.aligner = null;
        }

        if (useCache) {
            this.softCache
                    = new SoftReference<HashMap<Pair<ProtGroupMember>,Double>>
                    (new HashMap<Pair<ProtGroupMember>,Double>(512));
        } else {
            this.softCache = null;
        }
    }
    
    /**
     * Returns a similarity score for a protein pair. The higher the score, the
     * more similar the proteins are. Higher means more similar. If the pair 
     * doesn't go over the threshold, null is returned. The order of proteins 
     * doesn't matter, because similarity(p1,p2) == 
     *                         max(similarity(p2,p1), similarity(p1,p2)).
     * @param p1 protein 1
     * @param p2 protein 2
     * @return similarity score, or null if not over threshold
     */
    public synchronized final Double similarity(final ProtGroupMember p1,
                                                final ProtGroupMember p2)
    {
        final Pair<ProtGroupMember> pp = new Pair<ProtGroupMember>(p1,p2);
        
        // Use containsKey before get, because softCache might contain nulls
        if (usesCache() && this.getCache().containsKey(pp)) { // check softCache first
            ++this.cacheHits;
            return this.getCache().get(pp); // return cached score
        } else {
            ++this.cacheMisses;
        }
        

        Double score = null; // must be null first

        // If allowed, try scoring by accession equality, regardless of method
        if (this.passByAccession) {
            score = this.scoreByAcc(p1, p2);
            if (score!=null) { // take the easy way out
                if (usesCache()) { this.getCache().put(pp,score); }
                return score;
            }
        }

        switch(this.method) {
            case AccessionOnly :
                score = this.scoreByAcc(p1, p2);
                break;
            case PeptideSubset :
                score = scoreByPepSubset(p1, p2, this.strictPep, this.minPepsForCommonPeps);
                break;
            case CommonPeptides :
                score = scoreByCommonPeps(p1, p2, this.strictPep, this.minPepsForCommonPeps);
                break;
            case Sequence :
                score = scoreBySequence(p1, p2, this.maxAAsubsts);
                break;
            default :
                throw new RuntimeException("Programming error: new grouping" +
                        "methods added, but not used everywhere. Aborting.");
        }

        if (usesCache()) { this.getCache().put(pp,score); }

        return score; // return resulting score, might be null
    }

    /**
     * Returns true, if this instance uses a softCache for similarity scores.
     * @return true if softCache used, false otherwise
     */
    public final boolean usesCache() {
        return !(this.softCache==null);
    }

    /**
     * Clears the similarity score softCache.
     */
    public synchronized final void clearCache() {
        if (this.softCache!=null) {
            this.softCache.clear();
        }
    }

    /**
     * Returns the amount of softCache hits so far. If softCache is not used, 0 is
     * returned
     * @return softCache hits
     */
    public synchronized final long cacheHits() {
        return this.cacheHits;
    }

    /**
     * Returns the amount of softCache misses so far. If softCache is not used, this
     * will increase anyway.
     * @return softCache misses
     */
    public synchronized final long cacheMisses() {
        return this.cacheMisses;
    }

    /**
     * Returns the number of elements currently in the similarity score softCache.
     * If softCache is not used, 0 is returned.
     * @return amount of elements in the softCache currently
     */
    public synchronized int cachedElementsCount() {
        if (this.softCache==null) {
            return 0;
        }
        return (this.getCache().size());
    }


    private synchronized Double scoreByAcc(ProtGroupMember p1,
                                           ProtGroupMember p2)
    {
        if (p1.getAccession().equals(p2.getAccession())) {
            return EQUALACCESSIONSCORE;
        }
        return null;
    }

    private synchronized Double scoreByPepSubset(final ProtGroupMember p1,
                                                 final ProtGroupMember p2,
                                                 final boolean strict,
                                                 int minCommon)
    {

        if (!p1.hasQueries() || !p2.hasQueries()) {
            return null;
        }

        Set<AminoSequence> pSmall = p1.getDistinctPeptideSequences();
        Set<AminoSequence> pBig = p2.getDistinctPeptideSequences();

        if (pBig.size() < pSmall.size()) {
            Set<AminoSequence> pTemp = pBig;
            pBig = pSmall;
            pSmall = pTemp;
        }

        if (pSmall.size()<minCommon) { // not enough peptides
            return null;
        } else {
            minCommon = pSmall.size(); // use subset!
        }

        int common = this.commonPepsCount(pSmall, pBig, strict);

        if (common < minCommon) {
            return null;
        }

        return common / (double)(pSmall.size()+pBig.size());


    }

    /**
     * Returns a similarity score for two proteins by their shared peptides
     * @param p1 protein 1
     * @param p2 protein 2
     * @param strict if true, peptide sequences will be compared using exact
     *        matching amino acid by amino acid. if false, loose comparison
     *        with ambiguous AA's is used (ie X==any, etc).
     * @return similarity score
     */
    private synchronized Double scoreByCommonPeps(final ProtGroupMember p1,
                                                  final ProtGroupMember p2,
                                                  final boolean strict,
                                                  final int minCommon)
    {
        if (!p1.hasQueries() || !p2.hasQueries()) {
            return null;
        }

        Set<AminoSequence> pSmall = p1.getDistinctPeptideSequences();
        Set<AminoSequence> pBig = p2.getDistinctPeptideSequences();

        if (pBig.size() < pSmall.size()) {
            Set<AminoSequence> pTemp = pBig;
            pBig = pSmall;
            pSmall = pTemp;
        }

        if (pSmall.size()<minCommon) { // not enough peptides
            return null;
        }

        int common = this.commonPepsCount(pSmall, pBig, strict);

        if (common < minCommon) {
            return null;
        }

        return common / (double)(pSmall.size()+pBig.size());
    }

    private synchronized int commonPepsCount(Set<AminoSequence> pSmall, Set<AminoSequence> pBig, boolean strict) {

        if (pBig.size() < pSmall.size()) { // make sure small is small
            Set<AminoSequence> pTemp = pBig;
            pBig = pSmall;
            pSmall = pTemp;
        }

        int common = 0;

        if (strict) {
            for (AminoSequence pepSmall : pSmall) {
                if (pBig.contains(pepSmall)) {
                    ++common;
                }
            }
        } else {
            for (AminoSequence pepSmall : pSmall) {
                for (AminoSequence pepBig : pBig) {
                    if (pepSmall.equalsLoosely(pepBig)) {
                        ++common;
                        break;
                    }
                }
            }
        }

        return common;
    }

    private synchronized Double scoreBySequence(ProtGroupMember p1, ProtGroupMember p2, int maxSubsts) {
        if (!p1.hasSequence() || !p2.hasSequence()) {
            return null;
        }

        AminoSequence seq1 = p1.getSequence();
        AminoSequence seq2 = p2.getSequence();

        // unknown sequences don't count
        if (seq1.getLength()<1 || seq2.getLength()<1) {
            return null;
        }

        // if same object, return best possible similarity score
        if (seq1==seq2) {
            return Double.MAX_VALUE;
        }

        // if lengths are the same, do speedy comparison
        if (seq1.getLength()==seq2.getLength() && seq1.equalsLoosely(seq2)) {
            return Double.MAX_VALUE-10;
        }

        // align only if lengths are close enough
        if (maxSubsts>0 && Math.abs(seq1.getLength()-seq2.getLength())<=maxSubsts) {
            try {
                Alignment al = aligner.align(seq1, seq2);
                if (al.getSubstitutionCount()<=maxSubsts) {
                    return al.getScore();
                }
            } catch (Exception ex) {
                System.out.println("DEBUG: alignment failed");
            }
        }

        return null; // not over threshold

    }

    private synchronized HashMap<Pair<ProtGroupMember>,Double> getCache() {
        HashMap<Pair<ProtGroupMember>,Double> hm = this.softCache.get();
        if (hm==null) {
            System.out.println("DEBUG: ProteinPairScorer cache has been cleaned before");
            hm = new HashMap<Pair<ProtGroupMember>,Double>(512);
            this.softCache = new SoftReference<HashMap<Pair<ProtGroupMember>,Double>>(hm);
        }
        return hm;
    }

}
