/*
 * 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 java.util.Arrays;

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

/** BandedMatrix - class which used to store alignment matrix for {@link BandedLinearAligner}. */
public final class BandedMatrix implements java.io.Serializable {
    /** Value returned for cells outside the band */
    public static final int DEFAULT_VALUE = Integer.MIN_VALUE / 2;
    /** Data */
    private final int[] matrix;
    /** Number of rows */
    private final int size1;
    /** Number of columns */
    private final int size2;
    /** Row length */
    private final int rowFactor;
    /** Width in the first index dimension (vertical axes; i) */
    private final int width1;
    /** Width in the first index dimension (vertical axes; i) */
    private final int width2;

    @Deprecated
    public BandedMatrix(CachedIntArray cachedArray, int size1, int size2, int width) {
        this(cachedArray, size1, size2,
                1 + width + max(0, size1 - size2),
                1 + width + max(0, size2 - size1));
        // this.size1 = size1;
        // this.size2 = size2;
        // if (width >= size1)
        //     width = size1 - 1;
        // if (width >= size2)
        //     width = size2 - 1;
        // this.rowFactor = 2 * width + Math.abs(size2 - size1);
        // this.zeroDelta = -Math.min(0, size2 - size1) + width;
        // this.matrix = cachedArray.get((size1 - 1) * rowFactor + zeroDelta + size2);
    }

    public BandedMatrix(CachedIntArray cachedArray, int size1, int size2, int width1, int width2) {
        this.width1 = width1 = min(width1, size1);
        this.width2 = width2 = min(width2, size2);
        if (width1 <= max(0, size1 - size2))
            throw new IllegalArgumentException("Illegal value for width1");
        if (width2 <= max(0, size2 - size1))
            throw new IllegalArgumentException("Illegal value for width2");
        this.size1 = size1;
        this.size2 = size2;
        this.rowFactor = width1 + width2 - 2; // == rowLength - 1
        this.matrix = cachedArray.get(size1 * (rowFactor + 1)); // slight over-allocation of memory in the sake of simplicity
        // Arrays.fill(matrix, 0, (size1 - 1) * rowFactor + size2 - 1, DEFAULT_VALUE);
    }

    public int getRowLength() {
        return rowFactor + 1;
    }

    /** Inclusive lower boundary of range of defined values for index 1 given index 2 */
    public int index1From(int idx2) {
        return max(0, idx2 - width2 + 1);
    }

    /**
     * Special case for inclusive lower boundary of range of defined values for index 1 given index 2,
     * values are maxed method, useful for setting up iteration ranges in algorithms where first row has a
     * special purpose
     */
    public int index1FromNo0(int idx2) {
        return max(1, idx2 - width2 + 1);
    }

    /** Exclusive upper boundary of range of defined values for index 1 given index 2 */
    public int index1To(int idx2) {
        return min(idx2 + width1, size1);
    }

    /** Inclusive lower boundary of range of defined values for index 2 given index 1 */
    public int index2From(int idx1) {
        return max(0, idx1 - width1 + 1);
    }

    /**
     * Special case for inclusive lower boundary of range of defined values for index 2 given index 1,
     * values are maxed method, useful for setting up iteration ranges in algorithms where first column has a
     * special purpose
     */
    public int index2FromNo0(int idx1) {
        return max(1, idx1 - width1 + 1);
    }

    /** Exclusive upper boundary of range of defined values for index 2 given index 1 */
    public int index2To(int idx1) {
        return min(idx1 + width2, size2);
    }

    public int getRowFactor() {
        return 0;
    }

    public int getZeroDelta() {
        return 0;
    }

    public int get(int i, int j) {
        return defined(i, j)
                ? matrix[i * rowFactor + j]
                : DEFAULT_VALUE;
    }

    public void set(int i, int j, int value) {
        if (!defined(i, j))
            throw new IndexOutOfBoundsException("Trying to set values outside the matrix band.");
        matrix[i * rowFactor + j] = value;
    }

    public boolean defined(int i, int j) {
        if (i < 0 || j < 0 || i >= size1 || j >= size2)
            throw new IndexOutOfBoundsException();
        return index1From(j) <= i && i < index1To(j) &&
                index2From(i) <= j && j < index2To(i);
    }

    @Override
    public String toString() {
        return toString(null, null);
    }

    private static String vToString(int value) {
        return value == DEFAULT_VALUE
                ? "X"
                : Math.abs(value - DEFAULT_VALUE) < 1000
                ? "X+" + (value - DEFAULT_VALUE)
                : Integer.toString(value);
    }

    public String toString(String a, String b) {
        int maxLen = 0;
        for (int i : matrix)
            maxLen = max(maxLen, vToString(i).length());

        StringBuilder sb = new StringBuilder();
        if (b != null) {
            if (a != null)
                sb.append("  ");
            for (int j = 0; j < size2; j++) {
                if (j != 0)
                    sb.append(' ');
                for (int k = 0; k < maxLen - 1; k++)
                    sb.append(' ');
                sb.append(b.charAt(j));
            }
        }
        sb.append('\n');

        for (int i = 0; i < size1; i++) {
            if (a != null) {
                sb.append(a.charAt(i));
                sb.append(' ');
            }
            for (int j = 0; j < size2; j++) {
                sb.append('|');
                String s = defined(i, j) ? vToString(get(i, j)) : "";
                for (int k = 0; k < maxLen - s.length(); k++)
                    sb.append(' ');
                sb.append(s);
            }
            sb.append('\n');
        }
        return sb.toString();
    }
}
