/*
 * 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.sequence.SequenceQuality;
import com.milaboratory.util.BitArray;

import java.util.Arrays;

import static com.milaboratory.util.StringUtil.spaces;

public final class AlignmentHelper implements java.io.Serializable {
    protected final String seq1String, seq2String;
    protected final String markup;
    protected final int[] seq1Position, seq2Position;
    protected final BitArray match;
    protected final int offset;

    public AlignmentHelper(String seq1String, String seq2String, int[] seq1Position, int[] seq2Position, BitArray match) {
        this(seq1String, seq2String, seq1Position, seq2Position, match, 0);
    }

    public AlignmentHelper(String seq1String, String seq2String, int[] seq1Position, int[] seq2Position, BitArray match, int offset) {
        if (seq1Position.length == 0)
            seq1Position = new int[1];
        if (seq2Position.length == 0)
            seq2Position = new int[1];

        this.seq1String = seq1String;
        this.seq2String = seq2String;
        this.seq1Position = seq1Position;
        this.seq2Position = seq2Position;
        this.match = match;
        this.offset = Math.max(Math.max(("" + aL(seq2Position[0])).length(), ("" + aL(seq1Position[0])).length()), offset);

        char[] chars = new char[match.size()];
        Arrays.fill(chars, ' ');
        for (int n : match.getBits())
            chars[n] = '|';

        this.markup = new String(chars);
    }

    public AlignmentHelper getRange(int from, int to) {
        return getRange(from, to, 0);
    }

    public AlignmentHelper getRange(int from, int to, int offset) {
        return new AlignmentHelper(seq1String.substring(from, to), seq2String.substring(from, to),
                Arrays.copyOfRange(seq1Position, from, to), Arrays.copyOfRange(seq2Position, from, to),
                match.getRange(from, to), offset);
    }

    public AlignmentHelper[] split(int length) {
        return split(length, 0);
    }

    public AlignmentHelper[] split(int length, int offset) {
        AlignmentHelper[] ret = new AlignmentHelper[(size() + length - 1) / length];
        for (int i = 0; i < ret.length; ++i) {
            int pointer = i * length;
            int l = Math.min(length, size() - pointer);
            ret[i] = getRange(pointer, pointer + l, offset);
        }
        return ret;
    }

    /**
     * Gets the identity of alignment, i.e. ratio of matching nucleotides in the aligned region
     */
    public double identity() {
        return match.bitCount() * 1.0 / match.size();
    }

    /**
     * Gets the size of aligned region
     */
    public int size() {
        return match.size();
    }

    public int getSequence1PositionAt(int i) {
        return seq1Position[i];
    }

    public int getSequence2PositionAt(int i) {
        return seq2Position[i];
    }

    /**
     * Get the aligned query sequence
     */
    public String getSeq1String() {
        return seq1String;
    }

    /**
     * Get the aligned subject sequence
     */
    public String getSeq2String() {
        return seq2String;
    }

    /**
     * Get the alignment markup line
     */
    public String getMarkup() {
        return markup;
    }

    /**
     * Gets the first line of formatted alignment (query sequence)
     */
    public String getLine1() {
        String startPosition = String.valueOf(aL(seq1Position[0]));
        int spaces = offset - startPosition.length();
        return spaces(spaces) + startPosition + " " + seq1String +
                " " + aR(seq1Position[seq1Position.length - 1]);
    }

    public String getLine1Compact() {
        String startPosition = String.valueOf(aL(seq1Position[0]));
        int spaces = offset - startPosition.length();
        return spaces(spaces) + startPosition + " " + toCompact(seq1String) +
                " " + aR(seq1Position[seq1Position.length - 1]);
    }

    /**
     * Gets the second line of formatted alignment (alignment markup)
     */
    public String getLine2() {
        return spaces(offset + 1) + markup;
    }

    /**
     * Gets the third line of formatted alignment (subject sequence)
     */
    public String getLine3() {
        String startPosition = String.valueOf(aL(seq2Position[0]));
        int spaces = offset - startPosition.length();
        return spaces(spaces) + startPosition + " " + seq2String +
                " " + aR(seq2Position[seq2Position.length - 1]);
    }

    public String toStringWithSeq2Quality(SequenceQuality quality) {
        char[] chars = new char[size()];
        for (int i = 0; i < size(); ++i)
            chars[i] = seq2Position[i] < 0 ? ' ' : (char) (33 + quality.value(seq2Position[i]));
        return getLine1() + "\n" + getLine2() + "\n" + getLine3() + "\n" + spaces(offset + 1) + new String(chars);
    }

    public String getLine3Compact() {
        String startPosition = String.valueOf(aL(seq2Position[0]));
        int spaces = offset - startPosition.length();
        return spaces(spaces) + startPosition + " " + toCompact(seq2String) +
                " " + aR(seq2Position[seq2Position.length - 1]);
    }

    private String toCompact(String seqString) {
        char[] chars = seqString.toCharArray();
        for (int i = 0; i < match.size(); ++i)
            if (match.get(i))
                chars[i] = Character.toLowerCase(chars[i]);
        return new String(chars);
    }

    @Override
    public String toString() {
        return getLine1() + "\n" + getLine2() + "\n" + getLine3();
    }

    /**
     * Gets the alignment string in contact format (no markup line, mismatches shown by lower-case characters)
     */
    public String toCompactString() {
        return getLine1Compact() + "\n" + getLine3Compact();
    }

    private static int aL(int f) {
        return f < 0 ? ~f : f;
    }

    private static int aR(int f) {
        return f < 0 ? ~f - 1 : f;
    }
}
