/*
 * Copyright 2011-2013, by Vladimir Kostyukov and Contributors.
 * 
 * This file is part of la4j project (http://la4j.org)
 * 
 * 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.
 * 
 * Contributor(s): -
 * 
 */

package org.la4j.matrix.dense;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Random;

import org.la4j.Matrices;
import org.la4j.Matrix;
import org.la4j.matrix.DenseMatrix;
import org.la4j.matrix.MatrixFactory;
import org.la4j.Vector;
import org.la4j.vector.dense.BasicVector;

public class Basic2DMatrix extends DenseMatrix {

    private static final byte MATRIX_TAG = (byte) 0x10;

    /**
     * Creates a zero {@link Basic2DMatrix} of the given shape:
     * {@code rows} x {@code columns}.
     */
    public static Basic2DMatrix zero(int rows, int columns) {
        return new Basic2DMatrix(rows, columns);
    }

    /**
     * Creates a constant {@link Basic2DMatrix} of the given shape and {@code value}.
     */
    public static Basic2DMatrix constant(int rows, int columns, double constant) {
        double array[][] = new double[rows][columns];

        for (int i = 0; i < rows; i++) {
            Arrays.fill(array[i], constant);
        }

        return new Basic2DMatrix(array);
    }

    /**
     * Creates a diagonal {@link Basic2DMatrix} of the given {@code size} whose
     * diagonal elements are equal to {@code diagonal}.
     */
    public static Basic2DMatrix diagonal(int size, double diagonal) {
        double array[][] = new double[size][size];

        for (int i = 0; i < size; i++) {
            array[i][i] = diagonal;
        }

        return new Basic2DMatrix(array);
    }

    /**
     * Creates an unit {@link Basic2DMatrix} of the given shape:
     * {@code rows} x {@code columns}.
     */
    public static Basic2DMatrix unit(int rows, int columns) {
        return Basic2DMatrix.constant(rows, columns, 1.0);
    }

    /**
     * Creates an identity {@link Basic2DMatrix} of the given {@code size}.
     */
    public static Basic2DMatrix identity(int size) {
        return Basic2DMatrix.diagonal(size, 1.0);
    }

    /**
     * Creates a random {@link Basic2DMatrix} of the given shape:
     * {@code rows} x {@code columns}.
     */
    public static Basic2DMatrix random(int rows, int columns, Random random) {
        double array[][] = new double[rows][columns];

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < columns; j++) {
                array[i][j] = random.nextDouble();
            }
        }

        return new Basic2DMatrix(array);
    }

    /**
     * Creates a random symmetric {@link Basic2DMatrix} of the given {@code size}.
     */
    public static Basic2DMatrix randomSymmetric(int size, Random random) {
        double array[][] = new double[size][size];

        for (int i = 0; i < size; i++) {
            for (int j = i; j < size; j++) {
                double value = random.nextDouble();
                array[i][j] = value;
                array[j][i] = value;
            }
        }

        return new Basic2DMatrix(array);
    }

    /**
     * Creates a {@link Basic2DMatrix} of the given 1D {@code array} with
     * copying the underlying array.
     */
    public static Basic2DMatrix from1DArray(int rows, int columns, double[] array) {
        double[][] array2D = new double[rows][columns];

        for (int i = 0; i < rows; i++) {
            System.arraycopy(array, i * columns, array2D[i], 0, columns);
        }

        return new Basic2DMatrix(array2D);
    }

    /**
     * Creates a {@link Basic2DMatrix} of the given 2D {@code array} w/o
     * copying the underlying array.
     */
    public static Basic2DMatrix from2DArray(double[][] array) {
        return new Basic2DMatrix(array);
    }

    /**
     * Creates a block {@link Basic2DMatrix} of the given blocks {@code a},
     * {@code b}, {@code c} and {@code d}.
     */
    public static Basic2DMatrix block(Matrix a, Matrix b, Matrix c, Matrix d) {
        if ((a.rows() != b.rows()) || (a.columns() != c.columns()) ||
            (c.rows() != d.rows()) || (b.columns() != d.columns())) {
            throw new IllegalArgumentException("Sides of blocks are incompatible!");
        }

        int rows = a.rows() + c.rows(), columns = a.columns() + b.columns();
        double array[][] = new double[rows][columns];

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < columns; j++) {
                if ((i < a.rows()) && (j < a.columns())) {
                    array[i][j] = a.get(i, j);
                }
                if ((i < a.rows()) && (j > a.columns())) {
                    array[i][j] = b.get(i, j);
                }
                if ((i > a.rows()) && (j < a.columns())) {
                    array[i][j] = c.get(i, j);
                }
                if ((i > a.rows()) && (j > a.columns())) {
                    array[i][j] = d.get(i, j);
                }
            }
        }

        return new Basic2DMatrix(array);
    }

    /**
     * Decodes {@link Basic2DMatrix} from the given byte {@code array}.
     *
     * @param array the byte array representing a matrix
     *
     * @return a decoded matrix
     */
    public static Basic2DMatrix fromBinary(byte[] array) {
        ByteBuffer buffer = ByteBuffer.wrap(array);

        if (buffer.get() != MATRIX_TAG) {
            throw new IllegalArgumentException("Can not decode Basic2DMatrix from the given byte array.");
        }

        int rows = buffer.getInt();
        int columns = buffer.getInt();
        double[][] values = new double[rows][columns];

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < columns; j++) {
                values[i][j] = buffer.getDouble();
            }
        }

        return new Basic2DMatrix(values);
    }

    /**
     * Parses {@link Basic2DMatrix} from the given CSV string.
     *
     * @param csv the CSV string representing a matrix
     *
     * @return a parsed matrix
     */
    public static Basic2DMatrix fromCSV(String csv) {
        return Matrix.fromCSV(csv).to(Matrices.BASIC_2D);
    }

    /**
     * Parses {@link Basic2DMatrix} from the given Matrix Market string.
     *
     * @param mm the string in Matrix Market format
     *
     * @return a parsed matrix
     */
    public static Basic2DMatrix fromMatrixMarket(String mm) {
        return Matrix.fromMatrixMarket(mm).to(Matrices.BASIC_2D);
    }

    private double self[][];

    public Basic2DMatrix() {
        this(0, 0);
    }

    public Basic2DMatrix(int rows, int columns) {
        this(new double[rows][columns]);
    }

    public Basic2DMatrix(double array[][]) {
        super(array.length, array.length == 0 ? 0: array[0].length);
        this.self = array;
    }

    @Override
    public double get(int i, int j) {
        return self[i][j];
    }

    @Override
    public void set(int i, int j, double value) {
        self[i][j] = value;
    }

    @Override
    public void setAll(double value) {
        for (int i = 0; i < rows; i++) {
            Arrays.fill(self[i], value);
        }
    }

    @Override
    public void swapRows(int i, int j) {
        if (i != j) {
            double tmp[] = self[i];
            self[i] = self[j];
            self[j] = tmp;
        }
    }

    @Override
    public void swapColumns(int i, int j) {
        if (i != j) {
            for (int ii = 0; ii < rows; ii++) {
                double tmp = self[ii][i];
                self[ii][i] = self[ii][j];
                self[ii][j] = tmp;
            }
        }
    }

    @Override
    public Vector getRow(int i) {
        double result[] = new double[columns];
        System.arraycopy(self[i], 0, result, 0, columns);

        return new BasicVector(result);
    }

    @Override
    public Matrix copyOfShape(int rows, int columns) {
        ensureDimensionsAreCorrect(rows, columns);

        double $self[][] = new double[rows][columns];
        for (int i = 0; i < Math.min(this.rows, rows); i++) {
            System.arraycopy(self[i], 0, $self[i], 0, Math.min(this.columns, columns));
        }

        return new Basic2DMatrix($self);
    }

    @Override
    public double[][] toArray() {
        double result[][] = new double[rows][columns];

        for (int i = 0; i < rows; i++) {
            System.arraycopy(self[i], 0, result[i], 0, columns);
        }

        return result;
    }

    @Override
    public <T extends Matrix> T to(MatrixFactory<T> factory) {
        if (factory.outputClass == Basic2DMatrix.class) {
            return factory.outputClass.cast(this);
        }

        return super.to(factory);
    }

    @Override
    public Matrix blankOfShape(int rows, int columns) {
        return Basic2DMatrix.zero(rows, columns);
    }

    @Override
    public byte[] toBinary() {
        int size = 1 +                  // 1 byte: class tag
                   4 +                  // 4 bytes: rows
                   4 +                  // 4 bytes: columns
                  (8 * rows * columns); // 8 * rows * columns bytes: values

        ByteBuffer buffer = ByteBuffer.allocate(size);

        buffer.put(MATRIX_TAG);
        buffer.putInt(rows);
        buffer.putInt(columns);
        for (int i = 0; i < rows; i++) {
            for (double value : self[i]) {
                buffer.putDouble(value);
            }
        }

        return buffer.array();
    }
}
