/*
 * Copyright (c) 2022 MiLaboratories Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.milaboratory.core.alignment;

import com.milaboratory.core.Range;
import com.milaboratory.core.mutations.MutationsBuilder;
import com.milaboratory.core.sequence.NucleotideSequence;

import static java.lang.Math.max;
import static java.lang.Math.min;

public final class BandedLinearAligner {
    private BandedLinearAligner() {
    }

    /**
     * Classical Banded Alignment
     * <p/>
     * <p>Both sequences must be highly similar</p> <p>Align 2 sequence completely (i.e. while first sequence will be
     * aligned against whole second sequence)</p>
     *
     * @param scoring     scoring system
     * @param seq1        first sequence
     * @param seq2        second sequence
     * @param offset1     offset in first sequence
     * @param length1     length of first sequence's part to be aligned
     * @param offset2     offset in second sequence
     * @param length2     length of second sequence's part to be aligned
     * @param width       width of banded alignment matrix. In other terms max allowed number of indels
     * @param mutations   mutations array where all mutations will be kept
     * @param cachedArray cached (created once) array to be used in {@link BandedMatrix}, which is compact alignment
     *                    scoring matrix
     */
    public static float align0(LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
                               int offset1, int length1, int offset2, int length2,
                               int width, MutationsBuilder<NucleotideSequence> mutations, CachedIntArray cachedArray) {
        return align0(scoring, seq1, seq2, offset1, length1, offset2, length2,
                width + 1 + max(0, length1 - length2), width + 1 + max(0, length2 - length1),
                0, 0, 0, 0,
                mutations, cachedArray).score;
    }

    /**
     * Classical Banded Alignment
     * <p/>
     * <p>Both sequences must be highly similar</p> <p>Align 2 sequence completely (i.e. while first sequence will be
     * aligned against whole second sequence)</p>
     *
     * @param scoring     scoring system
     * @param seq1        first sequence
     * @param seq2        second sequence
     * @param offset1     offset in first sequence
     * @param length1     length of first sequence's part to be aligned
     * @param offset2     offset in second sequence
     * @param length2     length of second sequence's part to be aligned
     * @param width1      band width for axis 1 in alignment matrix
     * @param width2      band width for axis 2 in alignment matrix
     * @param leftAdded1  number of letters from the left side of sequence 1 for which to set zero penalty for deletion
     * @param rightAdded1 number of letters from the right side of sequence 1 for which to set zero penalty for deletion
     * @param leftAdded2  number of letters from the left side of sequence 2 for which to set zero penalty for insertion
     * @param rightAdded2 number of letters from the right side of sequence 2 for which to set zero penalty for insertion
     * @param mutations   mutations array where all mutations will be kept
     * @param cachedArray cached (created once) array to be used in {@link BandedMatrix}, which is compact
     *                    alignment scoring matrix
     */
    public static UniversalSemiGlobalAlignmentResult align0(
            LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
            int offset1, int length1, int offset2, int length2,
            int width1, int width2,
            int leftAdded1, int rightAdded1, int leftAdded2, int rightAdded2,
            MutationsBuilder<NucleotideSequence> mutations, CachedIntArray cachedArray) {
        if (offset1 < 0 || length1 < 0 || offset2 < 0 || length2 < 0)
            throw new IllegalArgumentException();

        if (width1 <= leftAdded1 || width1 <= rightAdded1 || width2 <= leftAdded2 || width2 <= rightAdded2)
            throw new IllegalArgumentException("Width smaller than or equal to the number of added letters for one of the sequences");

        if (offset1 + length1 > seq1.size() || offset2 + length2 > seq2.size())
            throw new IllegalArgumentException();

        int size1 = length1 + 1,
                size2 = length2 + 1;

        BandedMatrix matrix = new BandedMatrix(cachedArray, size1, size2, width1, width2);

        int i, j;

        // Filling initial values
        matrix.set(0, 0, 0);

        // Top row (leading insertions)
        for (j = 1; j <= leftAdded2; ++j)
            matrix.set(0, j, 0);
        for (j = leftAdded2 + 1; j < matrix.index2To(0); ++j)
            matrix.set(0, j, matrix.get(0, j - 1) +
                    (0 == length1 && j > length2 - rightAdded2 ? 0 : scoring.getGapPenalty()));

        // First column (leading deletions)
        for (i = 1; i <= leftAdded1; ++i)
            matrix.set(i, 0, 0);
        for (i = leftAdded1 + 1; i < matrix.index1To(0); ++i)
            matrix.set(i, 0, matrix.get(i - 1, 0) +
                    (0 == length2 && i > length1 - rightAdded1 ? 0 : scoring.getGapPenalty()));

        int match, delete, insert, to;

        // Forward propagation
        for (i = 1; i < size1; ++i) {
            to = matrix.index2To(i);
            for (j = matrix.index2FromNo0(i); j < to; ++j) {
                match = matrix.get(i - 1, j - 1) +
                        scoring.getScore(seq1.codeAt(offset1 + i - 1), seq2.codeAt(offset2 + j - 1));
                delete = matrix.get(i - 1, j) +
                        (j == length2 && i > length1 - rightAdded1 ? 0 : scoring.getGapPenalty());
                insert = matrix.get(i, j - 1) +
                        (i == length1 && j > length2 - rightAdded2 ? 0 : scoring.getGapPenalty());
                matrix.set(i, j, max(match, max(delete, insert)));
            }
        }

        // System.out.println(matrix.toString(
        //         " " + seq1.getRange(offset1, offset1 + length1).toString(),
        //         " " + seq2.getRange(offset2, offset2 + length2).toString()));

        int
                leftSkipped1 = 0,
                rightSkipped1 = 0,
                leftSkipped2 = 0,
                rightSkipped2 = 0;

        to = mutations.size();
        i = length1;
        j = length2;
        byte c1, c2;
        while (i > 0 || j > 0) {
            // TODO check this once again
            boolean
                    left1Edge = i == 0 && j <= leftAdded2,
                    right1Edge = i == length1 && j > length2 - rightAdded2,
                    left2Edge = j == 0 && i <= leftAdded1,
                    right2Edge = j == length2 && i > length1 - rightAdded1;

            assert !(left1Edge && right1Edge);
            assert !(left2Edge && right2Edge);

            if (i > 0 && j > 0 &&
                    matrix.get(i, j) == matrix.get(i - 1, j - 1) +
                            scoring.getScore(c1 = seq1.codeAt(offset1 + i - 1),
                                    c2 = seq2.codeAt(offset2 + j - 1))) {
                if (c1 != c2)
                    mutations.appendSubstitution(offset1 + i - 1, c1, c2);
                --i;
                --j;
            } else if (i > 0 &&
                    matrix.get(i, j) ==
                            matrix.get(i - 1, j) + (left2Edge || right2Edge ? 0 : scoring.getGapPenalty())) {
                if (left2Edge)
                    leftSkipped1++;
                else if (right2Edge)
                    rightSkipped1++;
                else
                    mutations.appendDeletion(offset1 + i - 1, seq1.codeAt(offset1 + i - 1));
                --i;
            } else if (j > 0 &&
                    matrix.get(i, j) ==
                            matrix.get(i, j - 1) + (left1Edge || right1Edge ? 0 : scoring.getGapPenalty())) {
                if (left1Edge)
                    leftSkipped2++;
                else if (right1Edge)
                    rightSkipped2++;
                else
                    mutations.appendInsertion(offset1 + i, seq2.codeAt(offset2 + j - 1));
                --j;
            } else
                throw new RuntimeException();
        }

        assert leftSkipped1 == 0 || leftSkipped2 == 0;
        assert rightSkipped1 == 0 || rightSkipped2 == 0;

        mutations.reverseRange(to, mutations.size());

        return new UniversalSemiGlobalAlignmentResult(
                matrix.get(length1 - rightSkipped1, length2 - rightSkipped2),
                leftSkipped1, rightSkipped1, leftSkipped2, rightSkipped2);
    }

    /**
     * Semi-semi-global alignment with artificially added letters.
     * <p/>
     * <p>Alignment where second sequence is aligned to the right part of first sequence.</p> <p>Whole second sequence
     * must be highly similar to the first sequence</p>
     *
     * @param scoring           scoring system
     * @param seq1              first sequence
     * @param seq2              second sequence
     * @param offset1           offset in first sequence
     * @param length1           length of first sequence's part to be aligned including artificially added letters
     * @param addedNucleotides1 number of artificially added letters to the first sequence
     * @param offset2           offset in second sequence
     * @param length2           length of second sequence's part to be aligned including artificially added letters
     * @param addedNucleotides2 number of artificially added letters to the second sequence
     * @param width             width of banded alignment matrix. In other terms max allowed number of indels
     * @param mutations         mutations array where all mutations will be kept
     * @param cachedArray       cached (created once) array to be used in {@link BandedMatrix}, which is compact
     *                          alignment scoring matrix
     */
    public static BandedSemiLocalResult alignRightAdded0(LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
                                                         int offset1, int length1, int addedNucleotides1, int offset2, int length2, int addedNucleotides2,
                                                         int width, MutationsBuilder<NucleotideSequence> mutations, CachedIntArray cachedArray) {
        addedNucleotides1 = min(addedNucleotides1, length1);
        addedNucleotides2 = min(addedNucleotides2, length2);
        int w1 = max(max(0, length1 - length2) + 1 + width, addedNucleotides1 + 1);
        int w2 = max(max(0, length2 - length1) + 1 + width, addedNucleotides2 + 1);
        UniversalSemiGlobalAlignmentResult res = align0(scoring, seq1, seq2,
                offset1, length1, offset2, length2,
                w1, w2,
                0, addedNucleotides1, 0, addedNucleotides2,
                mutations, cachedArray);
        return new BandedSemiLocalResult(
                offset1 + length1 - res.rightSkipped1 - 1,
                offset2 + length2 - res.rightSkipped2 - 1,
                res.score);

        // if (offset1 < 0 || length1 < 0 || offset2 < 0 || length2 < 0)
        //     throw new IllegalArgumentException();
        //
        // int size1 = length1 + 1,
        //         size2 = length2 + 1;
        //
        // BandedMatrix matrix = new BandedMatrix(cachedArray, size1, size2, width);
        //
        // int i, j;
        //
        // for (j = matrix.index2To(0) - 1; j > 0; --j)
        //     matrix.set(0, j, scoring.getGapPenalty() * j);
        //
        // for (i = matrix.index1To(0) - 1; i > 0; --i)
        //     matrix.set(i, 0, scoring.getGapPenalty() * i);
        //
        // matrix.set(0, 0, 0);
        //
        // int match, delete, insert, to;
        //
        // for (i = 0; i < length1; ++i) {
        //     to = matrix.index2To(i + 1) - 1;
        //     for (j = matrix.index2FromNo0(i + 1) - 1; j < to; ++j) {
        //         match = matrix.get(i, j) +
        //                 scoring.getScore(seq1.codeAt(offset1 + i), seq2.codeAt(offset2 + j));
        //         delete = matrix.get(i, j + 1) + scoring.getGapPenalty();
        //         insert = matrix.get(i + 1, j) + scoring.getGapPenalty();
        //         matrix.set(i + 1, j + 1, max(match, max(delete, insert)));
        //     }
        // }
        //
        // //Searching for max.
        // int maxI = -1, maxJ = -1;
        // int maxScore = Integer.MIN_VALUE;
        //
        // j = length2;
        // for (i = length1 - addedNucleotides1; i < size1; ++i)
        //     if (maxScore < matrix.get(i, j)) {
        //         maxScore = matrix.get(i, j);
        //         maxI = i;
        //         maxJ = j;
        //     }
        //
        // i = length1;
        // for (j = length2 - addedNucleotides2; j < size2; ++j)
        //     if (maxScore < matrix.get(i, j)) {
        //         maxScore = matrix.get(i, j);
        //         maxI = i;
        //         maxJ = j;
        //     }
        //
        // to = mutations.size();
        // i = maxI - 1;
        // j = maxJ - 1;
        // byte c1, c2;
        // while (i >= 0 || j >= 0) {
        //     if (i >= 0 && j >= 0 &&
        //             matrix.get(i + 1, j + 1) == matrix.get(i, j) +
        //                     scoring.getScore(c1 = seq1.codeAt(offset1 + i),
        //                             c2 = seq2.codeAt(offset2 + j))) {
        //         if (c1 != c2)
        //             mutations.appendSubstitution(offset1 + i, c1, c2);
        //         --i;
        //         --j;
        //     } else if (i >= 0 &&
        //             matrix.get(i + 1, j + 1) ==
        //                     matrix.get(i, j + 1) + scoring.getGapPenalty()) {
        //         mutations.appendDeletion(offset1 + i, seq1.codeAt(offset1 + i));
        //         --i;
        //     } else if (j >= 0 &&
        //             matrix.get(i + 1, j + 1) ==
        //                     matrix.get(i + 1, j) + scoring.getGapPenalty()) {
        //         mutations.appendInsertion(offset1 + i + 1, seq2.codeAt(offset2 + j));
        //         --j;
        //     } else
        //         throw new RuntimeException();
        // }
        //
        // mutations.reverseRange(to, mutations.size());
        //
        // return new BandedSemiLocalResult(offset1 + maxI - 1, offset2 + maxJ - 1, maxScore);
    }


    /**
     * Semi-semi-global alignment with artificially added letters.
     *
     * <p>Alignment where second sequence is aligned to the left part of first sequence.</p>
     *
     * <p>Whole second sequence must be highly similar to the first sequence, except last {@code width} letters, which
     * are to be checked whether they can improve alignment or not.</p>
     *
     * @param scoring           scoring system
     * @param seq1              first sequence
     * @param seq2              second sequence
     * @param offset1           offset in first sequence
     * @param length1           length of first sequence's part to be aligned
     * @param addedNucleotides1 number of artificially added letters to the first sequence;
     *                          must be 0 if seq1 is a pattern to match and seq2 is target sequence
     *                          where we search the pattern
     * @param offset2           offset in second sequence
     * @param length2           length of second sequence's part to be aligned
     * @param addedNucleotides2 number of artificially added letters to the second sequence;
     *                          if seq2 is target sequence where we search the pattern, this parameter must be equal
     *                          to maximum allowed number of indels (same as width parameter)
     * @param width             width of banded alignment matrix. In other terms max allowed number of indels
     * @param mutations         mutations array where all mutations will be kept
     * @param cachedArray       cached (created once) array to be used in {@link BandedMatrix}, which is compact
     *                          alignment scoring matrix
     */
    public static BandedSemiLocalResult alignLeftAdded0(LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
                                                        int offset1, int length1, int addedNucleotides1, int offset2, int length2, int addedNucleotides2,
                                                        int width, MutationsBuilder<NucleotideSequence> mutations, CachedIntArray cachedArray) {
        addedNucleotides1 = min(addedNucleotides1, length1);
        addedNucleotides2 = min(addedNucleotides2, length2);
        int w1 = max(max(0, length1 - length2) + 1 + width, addedNucleotides1 + 1);
        int w2 = max(max(0, length2 - length1) + 1 + width, addedNucleotides2 + 1);
        UniversalSemiGlobalAlignmentResult res = align0(scoring, seq1, seq2,
                offset1, length1, offset2, length2,
                w1, w2,
                addedNucleotides1, 0, addedNucleotides2, 0,
                mutations, cachedArray);
        return new BandedSemiLocalResult(
                offset1 + res.leftSkipped1,
                offset2 + res.leftSkipped2,
                res.score);

        // if (offset1 < 0 || length1 < 0 || offset2 < 0 || length2 < 0)
        //     throw new IllegalArgumentException();
        //
        // int size1 = length1 + 1,
        //         size2 = length2 + 1;
        //
        // BandedMatrix matrix = new BandedMatrix(cachedArray, size1, size2, width);
        //
        // int i, j;
        //
        // for (j = matrix.index2To(0) - 1; j > 0; --j)
        //     matrix.set(0, j, scoring.getGapPenalty() * j);
        //
        // for (i = matrix.index1To(0) - 1; i > 0; --i)
        //     matrix.set(i, 0, scoring.getGapPenalty() * i);
        //
        // matrix.set(0, 0, 0);
        //
        // int match, delete, insert, to;
        //
        // for (i = 0; i < length1; ++i) {
        //     to = matrix.index2To(i + 1) - 1;
        //     for (j = matrix.index2FromNo0(i + 1) - 1; j < to; ++j) {
        //         match = matrix.get(i, j) +
        //                 scoring.getScore(seq1.codeAt(offset1 + length1 - 1 - i),
        //                         seq2.codeAt(offset2 + length2 - 1 - j));
        //         delete = matrix.get(i, j + 1) + scoring.getGapPenalty();
        //         insert = matrix.get(i + 1, j) + scoring.getGapPenalty();
        //         matrix.set(i + 1, j + 1, max(match, max(delete, insert)));
        //     }
        // }
        //
        // //Searching for max.
        // int maxI = -1, maxJ = -1;
        // int maxScore = Integer.MIN_VALUE;
        //
        // j = length2;
        // for (i = length1 - addedNucleotides1; i < size1; ++i)
        //     if (maxScore < matrix.get(i, j)) {
        //         maxScore = matrix.get(i, j);
        //         maxI = i;
        //         maxJ = j;
        //     }
        //
        // i = length1;
        // for (j = length2 - addedNucleotides2; j < size2; ++j)
        //     if (maxScore < matrix.get(i, j)) {
        //         maxScore = matrix.get(i, j);
        //         maxI = i;
        //         maxJ = j;
        //     }
        //
        // i = maxI - 1;
        // j = maxJ - 1;
        // byte c1, c2;
        // while (i >= 0 || j >= 0) {
        //     if (i >= 0 && j >= 0 &&
        //             matrix.get(i + 1, j + 1) == matrix.get(i, j) +
        //                     scoring.getScore(c1 = seq1.codeAt(offset1 + length1 - 1 - i),
        //                             c2 = seq2.codeAt(offset2 + length2 - 1 - j))) {
        //         if (c1 != c2)
        //             mutations.appendSubstitution(offset1 + length1 - 1 - i, c1, c2);
        //         --i;
        //         --j;
        //     } else if (i >= 0 &&
        //             matrix.get(i + 1, j + 1) ==
        //                     matrix.get(i, j + 1) + scoring.getGapPenalty()) {
        //         mutations.appendDeletion(offset1 + length1 - 1 - i, seq1.codeAt(offset1 + length1 - 1 - i));
        //         --i;
        //     } else if (j >= 0 &&
        //             matrix.get(i + 1, j + 1) ==
        //                     matrix.get(i + 1, j) + scoring.getGapPenalty()) {
        //         mutations.appendInsertion(offset1 + length1 - 1 - i, seq2.codeAt(offset2 + length2 - 1 - j));
        //         --j;
        //     } else
        //         throw new RuntimeException();
        // }
        //
        // return new BandedSemiLocalResult(offset1 + length1 - maxI, offset2 + length2 - maxJ, maxScore);
    }


    /**
     * Alignment which identifies what is the highly similar part of the both sequences.
     * <p/>
     * <p>Alignment is done in the way that beginning of second sequences is aligned to beginning of first
     * sequence.</p>
     * <p/>
     * <p>Alignment terminates when score in banded alignment matrix reaches {@code stopPenalty} value.</p>
     * <p/>
     * <p>In other words, only left part of second sequence is to be aligned</p>
     *
     * @param scoring     scoring system
     * @param seq1        first sequence
     * @param seq2        second sequence
     * @param offset1     offset in first sequence
     * @param length1     length of first sequence's part to be aligned
     * @param offset2     offset in second sequence
     * @param length2     length of second sequence's part to be aligned@param width
     * @param stopPenalty alignment score value in banded alignment matrix at which alignment terminates
     * @param mutations   array where all mutations will be kept
     * @param cachedArray cached (created once) array to be used in {@link BandedMatrix}, which is compact alignment
     *                    scoring matrix
     * @return object which contains positions at which alignment terminated and array of mutations
     */
    public static BandedSemiLocalResult alignSemiLocalLeft0(LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
                                                            int offset1, int length1, int offset2, int length2,
                                                            int width, int stopPenalty, MutationsBuilder<NucleotideSequence> mutations,
                                                            CachedIntArray cachedArray) {
        if (offset1 < 0 || length1 < 0 || offset2 < 0 || length2 < 0)
            throw new IllegalArgumentException();

        int size1 = length1 + 1,
                size2 = length2 + 1;

        int matchReward = scoring.getScore((byte) 0, (byte) 0);

        BandedMatrix matrix = new BandedMatrix(cachedArray, size1, size2, width);

        int i, j;

        for (j = matrix.index2To(0) - 1; j > 0; --j)
            matrix.set(0, j, scoring.getGapPenalty() * j);

        for (i = matrix.index1To(0) - 1; i > 0; --i)
            matrix.set(i, 0, scoring.getGapPenalty() * i);

        matrix.set(0, 0, 0);

        int match, delete, insert, to;
        int max = 0;
        int iStop = 0, jStop = 0;
        int rowMax;

        for (i = 0; i < length1; ++i) {
            to = matrix.index2To(i + 1) - 1;
            rowMax = Integer.MIN_VALUE;
            for (j = matrix.index2FromNo0(i + 1) - 1; j < to; ++j) {
                match = matrix.get(i, j) +
                        scoring.getScore(seq1.codeAt(offset1 + i), seq2.codeAt(offset2 + j));
                delete = matrix.get(i, j + 1) + scoring.getGapPenalty();
                insert = matrix.get(i + 1, j) + scoring.getGapPenalty();
                matrix.set(i + 1, j + 1, match = max(match, max(delete, insert)));
                if (max < match) {
                    iStop = i + 1;
                    jStop = j + 1;
                    max = match;
                }
                rowMax = max(rowMax, match);
            }
            if (rowMax - i * matchReward < stopPenalty)
                break;
        }


        int fromL = mutations.size();

        i = iStop - 1;
        j = jStop - 1;
        byte c1, c2;
        while (i >= 0 || j >= 0) {
            if (i >= 0 && j >= 0 &&
                    matrix.get(i + 1, j + 1) == matrix.get(i, j) +
                            scoring.getScore(c1 = seq1.codeAt(offset1 + i),
                                    c2 = seq2.codeAt(offset2 + j))) {
                if (c1 != c2)
                    mutations.appendSubstitution(offset1 + i, c1, c2);
                --i;
                --j;
            } else if (i >= 0 &&
                    matrix.get(i + 1, j + 1) ==
                            matrix.get(i, j + 1) + scoring.getGapPenalty()) {
                mutations.appendDeletion(offset1 + i, seq1.codeAt(offset1 + i));
                --i;
            } else if (j >= 0 &&
                    matrix.get(i + 1, j + 1) ==
                            matrix.get(i + 1, j) + scoring.getGapPenalty()) {
                mutations.appendInsertion(offset1 + i + 1, seq2.codeAt(offset2 + j));
                --j;
            } else
                throw new RuntimeException();
        }

        mutations.reverseRange(fromL, mutations.size());

        return new BandedSemiLocalResult(offset1 + iStop - 1, offset2 + jStop - 1, max);
    }

    /**
     * Alignment which identifies what is the highly similar part of the both sequences.
     * <p/>
     * <p>Alignment is done in the way that end of second sequence is aligned to end of first sequence.</p> <p>Alignment
     * terminates when score in banded alignment matrix reaches {@code stopPenalty} value.</p> <p>In other words, only
     * right part of second sequence is to be aligned.</p>
     *
     * @param scoring     scoring system
     * @param seq1        first sequence
     * @param seq2        second sequence
     * @param offset1     offset in first sequence
     * @param length1     length of first sequence's part to be aligned
     * @param offset2     offset in second sequence
     * @param length2     length of second sequence's part to be aligned@param width
     * @param stopPenalty alignment score value in banded alignment matrix at which alignment terminates
     * @param mutations   array where all mutations will be kept
     * @param cachedArray cached (created once) array to be used in {@link BandedMatrix}, which is compact alignment
     *                    scoring matrix
     * @return object which contains positions at which alignment terminated and array of mutations
     */
    public static BandedSemiLocalResult alignSemiLocalRight0(LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
                                                             int offset1, int length1, int offset2, int length2,
                                                             int width, int stopPenalty, MutationsBuilder<NucleotideSequence> mutations,
                                                             CachedIntArray cachedArray) {
        if (offset1 < 0 || length1 < 0 || offset2 < 0 || length2 < 0)
            throw new IllegalArgumentException();

        int size1 = length1 + 1,
                size2 = length2 + 1;

        int matchReward = scoring.getScore((byte) 0, (byte) 0);

        BandedMatrix matrix = new BandedMatrix(cachedArray, size1, size2, width);

        int i, j;

        for (j = matrix.index2To(0) - 1; j > 0; --j)
            matrix.set(0, j, scoring.getGapPenalty() * j);

        for (i = matrix.index1To(0) - 1; i > 0; --i)
            matrix.set(i, 0, scoring.getGapPenalty() * i);

        matrix.set(0, 0, 0);

        int match, delete, insert, to;
        int max = 0;
        int iStop = 0, jStop = 0;
        int rowMax;

        for (i = 0; i < length1; ++i) {
            to = matrix.index2To(i + 1) - 1;
            rowMax = Integer.MIN_VALUE;
            for (j = matrix.index2FromNo0(i + 1) - 1; j < to; ++j) {
                match = matrix.get(i, j) +
                        scoring.getScore(seq1.codeAt(offset1 + length1 - 1 - i),
                                seq2.codeAt(offset2 + length2 - 1 - j));
                delete = matrix.get(i, j + 1) + scoring.getGapPenalty();
                insert = matrix.get(i + 1, j) + scoring.getGapPenalty();
                matrix.set(i + 1, j + 1, match = max(match, max(delete, insert)));
                if (max < match) {
                    iStop = i + 1;
                    jStop = j + 1;
                    max = match;
                }
                rowMax = max(rowMax, match);
            }
            if (rowMax - i * matchReward < stopPenalty)
                break;
        }

        i = iStop - 1;
        j = jStop - 1;
        byte c1, c2;
        while (i >= 0 || j >= 0) {
            if (i >= 0 && j >= 0 &&
                    matrix.get(i + 1, j + 1) == matrix.get(i, j) +
                            scoring.getScore(c1 = seq1.codeAt(offset1 + length1 - 1 - i),
                                    c2 = seq2.codeAt(offset2 + length2 - 1 - j))) {
                if (c1 != c2)
                    mutations.appendSubstitution(offset1 + length1 - 1 - i, c1, c2);
                --i;
                --j;
            } else if (i >= 0 &&
                    matrix.get(i + 1, j + 1) ==
                            matrix.get(i, j + 1) + scoring.getGapPenalty()) {
                mutations.appendDeletion(offset1 + length1 - 1 - i, seq1.codeAt(offset1 + length1 - 1 - i));
                --i;
            } else if (j >= 0 &&
                    matrix.get(i + 1, j + 1) ==
                            matrix.get(i + 1, j) + scoring.getGapPenalty()) {
                mutations.appendInsertion(offset1 + length1 - 1 - i, seq2.codeAt(offset2 + length2 - 1 - j));
                --j;
            } else
                throw new RuntimeException();
        }

        return new BandedSemiLocalResult(offset1 + length1 - iStop, offset2 + length2 - jStop, max);
    }


    /**
     * Classical Banded Alignment
     * <p/>
     * <p>Both sequences must be highly similar</p> <p>Align 2 sequence completely (i.e. while first sequence will be
     * aligned against whole second sequence)</p>
     *
     * @param scoring scoring system
     * @param seq1    first sequence
     * @param seq2    second sequence
     * @param width   width of banded alignment matrix. In other terms max allowed number of indels
     */
    public static Alignment<NucleotideSequence> align(LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
                                                      int width) {
        return align(scoring, seq1, seq2, 0, seq1.size(), 0, seq2.size(), width);
    }

    /**
     * Classical Banded Alignment
     * <p/>
     * <p>Both sequences must be highly similar</p> <p>Align 2 sequence completely (i.e. while first sequence will be
     * aligned against whole second sequence)</p>
     *
     * @param scoring scoring system
     * @param seq1    first sequence
     * @param seq2    second sequence
     * @param offset1 offset in first sequence
     * @param length1 length of first sequence's part to be aligned
     * @param offset2 offset in second sequence
     * @param length2 length of second sequence's part to be aligned
     * @param width   width of banded alignment matrix. In other terms max allowed number of indels
     */
    public static Alignment<NucleotideSequence> align(LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
                                                      int offset1, int length1, int offset2, int length2, int width) {
        try {
            MutationsBuilder<NucleotideSequence> mutations = new MutationsBuilder<>(NucleotideSequence.ALPHABET);
            float score = align0(scoring, seq1, seq2, offset1, length1, offset2, length2, width,
                    mutations, AlignmentCache.get());
            return new Alignment<>(seq1, mutations.createAndDestroy(),
                    new Range(offset1, offset1 + length1), new Range(offset2, offset2 + length2), score);
        } finally {
            AlignmentCache.release();
        }
    }

    /**
     * Classical Banded Alignment
     * <p/>
     * <p>Both sequences must be highly similar</p> <p>Align 2 sequence completely (i.e. while first sequence will be
     * aligned against whole second sequence)</p>
     *
     * @param scoring     scoring system
     * @param seq1        first sequence
     * @param seq2        second sequence
     * @param width1      band width for axis 1 in alignment matrix
     * @param width2      band width for axis 2 in alignment matrix
     * @param leftAdded1  number of letters from the left side of sequence 1 for which to set zero penalty for deletion
     * @param rightAdded1 number of letters from the right side of sequence 1 for which to set zero penalty for deletion
     * @param leftAdded2  number of letters from the left side of sequence 2 for which to set zero penalty for insertion
     * @param rightAdded2 number of letters from the right side of sequence 2 for which to set zero penalty for insertion
     */
    public static Alignment<NucleotideSequence> alignAdded(
            LinearGapAlignmentScoring<NucleotideSequence> scoring,
            NucleotideSequence seq1, NucleotideSequence seq2,
            int width1, int width2,
            int leftAdded1, int rightAdded1, int leftAdded2, int rightAdded2) {
        return alignAdded(scoring, seq1, seq2, 0, seq1.size(), 0, seq2.size(), width1, width2,
                leftAdded1, rightAdded1, leftAdded2, rightAdded2);
    }

    /**
     * Classical Banded Alignment
     * <p/>
     * <p>Both sequences must be highly similar</p> <p>Align 2 sequence completely (i.e. while first sequence will be
     * aligned against whole second sequence)</p>
     *
     * @param scoring     scoring system
     * @param seq1        first sequence
     * @param seq2        second sequence
     * @param offset1     offset in first sequence
     * @param length1     length of first sequence's part to be aligned
     * @param offset2     offset in second sequence
     * @param length2     length of second sequence's part to be aligned
     * @param width1      band width for axis 1 in alignment matrix
     * @param width2      band width for axis 2 in alignment matrix
     * @param leftAdded1  number of letters from the left side of sequence 1 for which to set zero penalty for deletion
     * @param rightAdded1 number of letters from the right side of sequence 1 for which to set zero penalty for deletion
     * @param leftAdded2  number of letters from the left side of sequence 2 for which to set zero penalty for insertion
     * @param rightAdded2 number of letters from the right side of sequence 2 for which to set zero penalty for insertion
     * @param arrayCache  array cache to reduce memory pressure
     */
    public static Alignment<NucleotideSequence> alignAdded(
            LinearGapAlignmentScoring<NucleotideSequence> scoring, NucleotideSequence seq1, NucleotideSequence seq2,
            int offset1, int length1, int offset2, int length2,
            int width1, int width2,
            int leftAdded1, int rightAdded1, int leftAdded2, int rightAdded2, CachedIntArray arrayCache) {
        MutationsBuilder<NucleotideSequence> mutations = new MutationsBuilder<>(NucleotideSequence.ALPHABET);
        UniversalSemiGlobalAlignmentResult result = align0(scoring, seq1, seq2, offset1, length1, offset2, length2,
                width1, width2,
                leftAdded1, rightAdded1, leftAdded2, rightAdded2,
                mutations, arrayCache);
        return new Alignment<>(seq1, mutations.createAndDestroy(),
                new Range(offset1 + result.leftSkipped1, offset1 + length1 - result.rightSkipped1),
                new Range(offset2 + result.leftSkipped2, offset2 + length2 - result.rightSkipped2),
                result.score);
    }

    /**
     * Classical Banded Alignment
     * <p/>
     * <p>Both sequences must be highly similar</p> <p>Align 2 sequence completely (i.e. while first sequence will be
     * aligned against whole second sequence)</p>
     *
     * @param scoring     scoring system
     * @param seq1        first sequence
     * @param seq2        second sequence
     * @param offset1     offset in first sequence
     * @param length1     length of first sequence's part to be aligned
     * @param offset2     offset in second sequence
     * @param length2     length of second sequence's part to be aligned
     * @param width1      band width for axis 1 in alignment matrix
     * @param width2      band width for axis 2 in alignment matrix
     * @param leftAdded1  number of letters from the left side of sequence 1 for which to set zero penalty for deletion
     * @param rightAdded1 number of letters from the right side of sequence 1 for which to set zero penalty for deletion
     * @param leftAdded2  number of letters from the left side of sequence 2 for which to set zero penalty for insertion
     * @param rightAdded2 number of letters from the right side of sequence 2 for which to set zero penalty for insertion
     */
    public static Alignment<NucleotideSequence> alignAdded(
            LinearGapAlignmentScoring<NucleotideSequence> scoring, NucleotideSequence seq1, NucleotideSequence seq2,
            int offset1, int length1, int offset2, int length2,
            int width1, int width2,
            int leftAdded1, int rightAdded1, int leftAdded2, int rightAdded2) {
        try {
            return alignAdded(scoring, seq1, seq2,
                    offset1, length1, offset2, length2,
                    width1, width2,
                    leftAdded1, rightAdded1, leftAdded2, rightAdded2,
                    AlignmentCache.get());
        } finally {
            AlignmentCache.release();
        }
    }

    /**
     * Semi-semi-global alignment with artificially added letters.
     *
     * <p>Alignment where second sequence is aligned to the left part of first sequence.</p>
     *
     * <p>Whole second sequence must be highly similar to the first sequence, except last {@code width} letters, which
     * are to be checked whether they can improve alignment or not.</p>
     *
     * @param scoring           scoring system
     * @param seq1              first sequence
     * @param seq2              second sequence
     * @param offset1           offset in first sequence
     * @param length1           length of first sequence's part to be aligned
     * @param addedNucleotides1 number of artificially added letters to the first sequence
     * @param offset2           offset in second sequence
     * @param length2           length of second sequence's part to be aligned
     * @param addedNucleotides2 number of artificially added letters to the second sequence
     * @param width             width of banded alignment matrix. In other terms max allowed number of indels
     */
    public static Alignment<NucleotideSequence> alignLeftAdded(LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
                                                               int offset1, int length1, int addedNucleotides1, int offset2, int length2, int addedNucleotides2,
                                                               int width) {
        try {
            MutationsBuilder<NucleotideSequence> mutations = new MutationsBuilder<>(NucleotideSequence.ALPHABET);
            BandedSemiLocalResult result = alignLeftAdded0(scoring, seq1, seq2,
                    offset1, length1, addedNucleotides1, offset2, length2, addedNucleotides2,
                    width, mutations, AlignmentCache.get());
            return new Alignment<>(seq1, mutations.createAndDestroy(),
                    new Range(result.sequence1Stop, offset1 + length1), new Range(result.sequence2Stop, offset2 + length2),
                    result.score);
        } finally {
            AlignmentCache.release();
        }
    }

    /**
     * Semi-semi-global alignment with artificially added letters.
     * <p/>
     * <p>Alignment where second sequence is aligned to the right part of first sequence.</p> <p>Whole second sequence
     * must be highly similar to the first sequence</p>
     *
     * @param scoring           scoring system
     * @param seq1              first sequence
     * @param seq2              second sequence
     * @param offset1           offset in first sequence
     * @param length1           length of first sequence's part to be aligned including artificially added letters
     * @param addedNucleotides1 number of artificially added letters to the first sequence
     * @param offset2           offset in second sequence
     * @param length2           length of second sequence's part to be aligned including artificially added letters
     * @param addedNucleotides2 number of artificially added letters to the second sequence
     * @param width             width of banded alignment matrix. In other terms max allowed number of indels
     */
    public static Alignment<NucleotideSequence> alignRightAdded(LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
                                                                int offset1, int length1, int addedNucleotides1, int offset2, int length2, int addedNucleotides2,
                                                                int width) {
        try {
            MutationsBuilder<NucleotideSequence> mutations = new MutationsBuilder<>(NucleotideSequence.ALPHABET);
            BandedSemiLocalResult result = alignRightAdded0(scoring, seq1, seq2,
                    offset1, length1, addedNucleotides1, offset2, length2, addedNucleotides2,
                    width, mutations, AlignmentCache.get());
            return new Alignment<>(seq1, mutations.createAndDestroy(),
                    new Range(offset1, result.sequence1Stop + 1), new Range(offset2, result.sequence2Stop + 1),
                    result.score);
        } finally {
            AlignmentCache.release();
        }
    }

    /**
     * Alignment which identifies what is the highly similar part of the both sequences.
     * <p/>
     * <p>Alignment is done in the way that beginning of second sequences is aligned to beginning of first
     * sequence.</p>
     * <p/>
     * <p>Alignment terminates when score in banded alignment matrix reaches {@code stopPenalty} value.</p>
     * <p/>
     * <p>In other words, only left part of second sequence is to be aligned</p>
     *
     * @param scoring     scoring system
     * @param seq1        first sequence
     * @param seq2        second sequence
     * @param offset1     offset in first sequence
     * @param length1     length of first sequence's part to be aligned
     * @param offset2     offset in second sequence
     * @param length2     length of second sequence's part to be aligned@param width
     * @param stopPenalty alignment score value in banded alignment matrix at which alignment terminates
     * @return object which contains positions at which alignment terminated and array of mutations
     */
    public static Alignment<NucleotideSequence> alignSemiLocalLeft(LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
                                                                   int offset1, int length1, int offset2, int length2,
                                                                   int width, int stopPenalty) {
        try {
            int minLength = min(length1, length2) + width + 1;
            length1 = min(length1, minLength);
            length2 = min(length2, minLength);
            MutationsBuilder<NucleotideSequence> mutations = new MutationsBuilder<>(NucleotideSequence.ALPHABET);
            BandedSemiLocalResult result = alignSemiLocalLeft0(scoring, seq1, seq2,
                    offset1, length1, offset2, length2, width, stopPenalty, mutations, AlignmentCache.get());
            return new Alignment<>(seq1, mutations.createAndDestroy(),
                    new Range(offset1, result.sequence1Stop + 1), new Range(offset2, result.sequence2Stop + 1),
                    result.score);
        } finally {
            AlignmentCache.release();
        }
    }

    /**
     * Alignment which identifies what is the highly similar part of the both sequences.
     * <p/>
     * <p>Alignment is done in the way that beginning of second sequences is aligned to beginning of first
     * sequence.</p>
     * <p/>
     * <p>Alignment terminates when score in banded alignment matrix reaches {@code stopPenalty} value.</p>
     * <p/>
     * <p>In other words, only left part of second sequence is to be aligned</p>
     *
     * @param scoring     scoring system
     * @param seq1        first sequence
     * @param seq2        second sequence
     * @param stopPenalty alignment score value in banded alignment matrix at which alignment terminates
     * @return object which contains positions at which alignment terminated and array of mutations
     */
    public static Alignment<NucleotideSequence> alignSemiLocalLeft(LinearGapAlignmentScoring scoring,
                                                                   NucleotideSequence seq1, NucleotideSequence seq2,
                                                                   int width, int stopPenalty) {
        return alignSemiLocalLeft(scoring, seq1, seq2, 0, seq1.size(), 0, seq2.size(), width, stopPenalty);
    }

    /**
     * Alignment which identifies what is the highly similar part of the both sequences.
     * <p/>
     * <p>Alignment is done in the way that end of second sequence is aligned to end of first sequence.</p> <p>Alignment
     * terminates when score in banded alignment matrix reaches {@code stopPenalty} value.</p> <p>In other words, only
     * right part of second sequence is to be aligned.</p>
     *
     * @param scoring     scoring system
     * @param seq1        first sequence
     * @param seq2        second sequence
     * @param offset1     offset in first sequence
     * @param length1     length of first sequence's part to be aligned
     * @param offset2     offset in second sequence
     * @param length2     length of second sequence's part to be aligned@param width
     * @param stopPenalty alignment score value in banded alignment matrix at which alignment terminates
     * @return object which contains positions at which alignment terminated and array of mutations
     */
    public static Alignment<NucleotideSequence> alignSemiLocalRight(LinearGapAlignmentScoring scoring, NucleotideSequence seq1, NucleotideSequence seq2,
                                                                    int offset1, int length1, int offset2, int length2,
                                                                    int width, int stopPenalty) {
        try {
            int minLength = min(length1, length2) + width + 1;
            int l1 = min(length1, minLength);
            int l2 = min(length2, minLength);
            offset1 = offset1 + length1 - l1;
            offset2 = offset2 + length2 - l2;
            length1 = l1;
            length2 = l2;
            MutationsBuilder<NucleotideSequence> mutations = new MutationsBuilder<>(NucleotideSequence.ALPHABET);
            BandedSemiLocalResult result = alignSemiLocalRight0(scoring, seq1, seq2,
                    offset1, length1, offset2, length2, width,
                    stopPenalty, mutations, AlignmentCache.get());
            return new Alignment<>(seq1, mutations.createAndDestroy(),
                    new Range(result.sequence1Stop, offset1 + length1), new Range(result.sequence2Stop, offset2 + length2),
                    result.score);
        } finally {
            AlignmentCache.release();
        }
    }

    /**
     * Alignment which identifies what is the highly similar part of the both sequences.
     * <p/>
     * <p>Alignment is done in the way that end of second sequence is aligned to end of first sequence.</p> <p>Alignment
     * terminates when score in banded alignment matrix reaches {@code stopPenalty} value.</p> <p>In other words, only
     * right part of second sequence is to be aligned.</p>
     *
     * @param scoring     scoring system
     * @param seq1        first sequence
     * @param seq2        second sequence
     * @param stopPenalty alignment score value in banded alignment matrix at which alignment terminates
     * @return object which contains positions at which alignment terminated and array of mutations
     */
    public static Alignment<NucleotideSequence> alignSemiLocalRight(LinearGapAlignmentScoring scoring,
                                                                    NucleotideSequence seq1, NucleotideSequence seq2,
                                                                    int width, int stopPenalty) {
        return alignSemiLocalRight(scoring, seq1, seq2, 0, seq1.size(), 0, seq2.size(), width, stopPenalty);
    }
}
