/*
 * 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.sequences;

import net.aavalla.utils.general.VirtualMatrix;
import java.util.HashMap;
import java.util.ListIterator;

/**
 *
 * @author Lari Natri
 */
public class BoundedAligner extends Aligner {
    
    private final int maxAlt;

    public BoundedAligner(int maxAlterations)
    {
        super(AminoAcid.getSimilarityMatrix(AminoAcid.AminoSMType.SIMPLE));
        this.maxAlt = maxAlterations;
    }

    @Override
    protected Alignment doActualAlignment(Sequence seqA, Sequence seqB) throws Exception
    {
        if (seqA == null || seqB == null) {
            throw new Exception("AlignmentProblem: null sequence");
        }

        if (this.maxAlt<1) { // no alterations allowed
            final Sequence[] seqs = new Sequence[2];
            seqs[0] = seqA;
            seqs[1] = seqB;
            @SuppressWarnings("unchecked")
            final HashMap<Integer, Integer>[] gaps = new HashMap[2];
            gaps[0] = new HashMap<Integer,Integer>(0,1);
            gaps[1] = gaps[0];

            // if same object, return best possible similarity score
            if (seqA==seqB) {
                return new Alignment(seqs, Double.MAX_VALUE, 0, gaps);
            }

            // if lengths are the same, do speedy comparison
            if (seqA.getLength()==seqB.getLength() && seqA.equalsLoosely(seqB)) {
                return new Alignment(seqs, Double.MAX_VALUE-10, 0, gaps);
            }
            return new Alignment(seqs, Double.NEGATIVE_INFINITY, Integer.MAX_VALUE, gaps);
        }

        int maxJUnderI = Math.max(maxAlt, maxAlt+seqA.getLength()-seqB.getLength());
        int maxJOverI = Math.max(maxAlt, maxAlt+seqB.getLength()-seqA.getLength());

        VirtualMatrix<Double> matrix = new VirtualMatrix<Double>(seqA.getLength()+1,
                seqB.getLength()+1, this.maxAlt, 0d, Double.NEGATIVE_INFINITY);

        int i;
        int j;
        for (i = 0; i <= seqA.getLength(); ++i) {
            matrix.set(i,0, i * simMatrix.getOpenGapScore());
        }
        for (j = 0; j <= seqB.getLength(); ++j) {
            matrix.set(0,j,j * simMatrix.getOpenGapScore());
        }

        double chNoGap;
        double chAGap;
        double chBGap;
        double thisPairScore;

        ListIterator<Component> aIter = seqA.iterator();
        ListIterator<Component> bIter = null;
        i = 0;
        Component a = null;
        Component b = null;
        while (aIter.hasNext()) {
            a = aIter.next();
            ++i;
            bIter = seqB.iterator();
            j = 0;
            while (bIter.hasNext()) {
                b = bIter.next();
                ++j;
                if (j < i-maxJUnderI) { // skip unneeded elements
                    continue;
                }
                if (j > i+maxJOverI) {
                    break;
                }
                thisPairScore = simMatrix.getSimilarityScore(a, b);
                chNoGap = matrix.get(i-1, j-1) + thisPairScore;
                chAGap = matrix.get(i-1,j) + simMatrix.getOpenGapScore();
                chBGap = matrix.get(i, j-1) + simMatrix.getOpenGapScore();
                // set best of three choices to matrix[i][j]
                matrix.set(i, j, chAGap);
                if (chBGap > matrix.get(i,j)) {
                    matrix.set(i, j, chBGap);
                }
                if (chNoGap > matrix.get(i,j)) {
                    matrix.set(i, j, chNoGap);
                }
            }
        }


        i = seqA.getLength();
        j = seqB.getLength();

        HashMap<Integer, Integer> aGaps = new HashMap<Integer, Integer>(8);
        HashMap<Integer, Integer> bGaps = new HashMap<Integer, Integer>(8);

        Integer oldGap = null;

        int substCount = 0;

        int oldI = -1;
        int oldJ = -1;

        // construct aligned strings
        while (i > 0 && j > 0) {
            if (oldI != i) {
                a = aIter.previous();
                oldI = i;
            }
            if (oldJ != j) {
                b = bIter.previous();
                oldJ = j;
            }
            //System.out.println(i + "_" + j + " " + substCount);
            thisPairScore = simMatrix.getSimilarityScore(a, b);

            if (matrix.get(i, j) == matrix.get(i-1,j-1) + thisPairScore) {
                if (a.equals(b) == false && a.equalsLoosely(b) == false) {
                    substCount++;
                }
                --i;
                --j;
            } else if (matrix.get(i, j) == matrix.get(i-1,j) + simMatrix.getOpenGapScore()) {
                oldGap = bGaps.get(j);
                if (oldGap == null) {
                    oldGap = 0;
                }
                ++oldGap;
                bGaps.put(j, oldGap);
                --i;
                ++substCount;
            } else {
                oldGap = aGaps.get(i);
                if (oldGap == null) {
                    oldGap = 0;
                }
                ++oldGap;
                aGaps.put(i, oldGap);
                --j;
                ++substCount;
            }
        }

        // add leading gapmarks
        while (aIter.hasPrevious()) {
            oldGap = bGaps.get(0);
            if (oldGap == null) {
                oldGap = 0;
            }
            ++oldGap;
            bGaps.put(0, oldGap);
            aIter.previous();
            ++substCount;
        }
        while (bIter.hasPrevious()) {
            oldGap = aGaps.get(0);
            if (oldGap == null) {
                oldGap = 0;
            }
            ++oldGap;
            aGaps.put(0, oldGap);
            bIter.previous();
            ++substCount;
        }

        final Sequence[] seqs = new Sequence[2];
        seqs[0] = seqA;
        seqs[1] = seqB;
        @SuppressWarnings("unchecked")
        final HashMap<Integer, Integer>[] gaps = new HashMap[2];
        gaps[0] = aGaps;
        gaps[1] = bGaps;

        return new Alignment(seqs, matrix.get(seqA.getLength(),seqB.getLength()), substCount, gaps);
        
    }
}
