package math.matrix;

import math.shapes.Line;
import math.utils.ArrayUtil;
import math.utils.MathUtil;

import javax.swing.*;
import java.io.*;

public class Matrix implements Cloneable, Externalizable {
    public static final String DERBY_CREATE_TYPE = "CREATE TYPE matrix "
            + "EXTERNAL NAME 'marix.Matrix' LANGUAGE JAVA";
    public static final String DERBY_CREATE_TABLE = "CREATE TABLE matrices("
            + "id INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS "
            + "IDENTITY(START WITH 1, INCREMENT BY 1),  macierz MATRIX)";
    private double[][] array;

    public Matrix(int m, int n) {
        array = new double[m][n];
    }

    public Matrix(double[][] array) {
        this.array = array;
    }

    public Matrix(double[] array, int cols) {
        this.array = ArrayUtil.oneToTwo(array, cols);
    }

    public Matrix(Matrix matrix) {
        this.array = matrix.array;
    }

    public Matrix(String path) {
        array = MatrixUtil.read(path);
    }

    public double[][] getArray() {
        return array;
    }

    public void setArray(double[][] array) {
        this.array = array;
    }

    /**
     * @return liczbê rzêdów
     */
    public int getRowCount() {
        return array.length;
    }

    /**
     * @return zwraca liczbê kolumn
     */
    public int getColCount() {
        return array[0].length;
    }

    /**
     * Drukuje tê macierz na konsoli
     */
    public void printToConsole() {
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                System.out.print(array[i][j] + " ");
            }
            System.out.println();
        }
    }

    /**
     * Zapisuje macierz do podanego pliku. Wartoci w rzêdzie
     * oddzielone s¹ spacj¹. Rz¹d koñczy siê rednikiem
     *
     * @param path - plik
     */
    public void write(String path) {
        try (PrintWriter pw = new PrintWriter(path)) {
            for (int i = 0; i < getRowCount(); i++) {
                for (int j = 0; j < getColCount() - 1; j++) {
                    pw.print(array[i][j] + " ");
                }
                pw.print(array[i][getColCount() - 1]);
                pw.print(";");
                pw.println();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * Zwraca glêbok¹ kopiê tej macierzy
     */
    @Override
    public Matrix clone() throws CloneNotSupportedException {
        Matrix cl = (Matrix) super.clone();
        cl.array = ArrayUtil.clone(array);
        return cl;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return false;
        }
        if (other == null) {
            return false;
        }
        if (getClass() != other.getClass()) {
            return false;
        }
        Matrix o = (Matrix) other;
        if (getRowCount() != o.getRowCount() || getColCount() != o.getColCount()) {
            return false;
        }
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                if (array[i][j] != o.array[i][j]) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        long b = 1L;
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                double cell = 0.0;
                try {
                    cell = getCell(i, j);
                } catch (MatrixException e) {
                    e.printStackTrace();
                }
                if (cell != 0.0) {
                    b = 31L * b + Double.doubleToLongBits(cell);
                } else {
                    b *= 31L;
                }
            }
        }
        return (int) (b ^ (b >> 32));
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                sb.append(array[i][j]).append(" ");
            }
            sb.append("\n");
        }
        return sb.toString();
    }

    /**
     * Ustawia wszystkie komórki macierzy na podan¹ wartoæ
     *
     * @param value - wartoæ do wstawienia
     */
    public void setToValue(double value) {
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                array[i][j] = value;
            }
        }
    }

    /**
     * Tworzy maicierz jednostkow¹. Na przek¹tnej s¹ jednoci, a
     * w pozosta³ych komórkach 0.
     *
     * @throws MatrixException
     */
    public void setToIdentity() throws MatrixException {
        if (!isSquareMatrix()) {
            throw new MatrixException("Macierz nie jest kwadratowa");
        }
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                if (i == j) {
                    array[i][j] = 1.0;
                } else {
                    array[i][j] = 0.0;
                }
            }
        }
    }

    /**
     * Sprawdza czy indeks rzêdu mieci siê w macierzy
     *
     * @param index
     * @return zwraca true jesli index jest w macierzy
     * false w przeciwnym wypadku
     */
    public boolean isRowIndexValid(int index) {
        return index >= 0 && index <= (getRowCount() - 1);
    }

    /**
     * Sprawdza czy indeks kolumny mieci siê w macierzy
     *
     * @param index
     * @return zwraca true jesli tal, false w przeciwnym wypadku
     */
    public boolean isColIndexValid(int index) {
        return index >= 0 && index <= (getColCount() - 1);
    }

    /**
     * Sprawdza czy indeksy kolumny i rzêdu s¹ w obrêbie macierzy.
     *
     * @param row
     * @param col
     * @return zweraca true jesli index jest w obrêbie macierzy, false
     * w przeciwnym wypadku
     * @throws MatrixException
     */
    public boolean isIndexValid(int row, int col) {
        return isRowIndexValid(row) && (isColIndexValid(col));
    }

    /**
     * Sprawdza czy macierz jest zerowa, tzn. czy wszystkie jej elementy
     * s¹ równe 0.0
     *
     * @return true jeli jest zerowa, false w przeciwnym wypadku
     */
    public boolean isZeroMatrix() {
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                if (array[i][j] != 0.0) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Sprawdza czy macierz jest jednostkowa. Macierz jest jednostkowa, gdy
     * wszystkie elementy macierzy s¹ równe 0, z wyj¹tkiem elementów le¿¹cych na
     * przek¹tnej, które s¹ równe 1.
     *
     * @return true jeli macierz jest elementarna, false w przeciwnym
     * przypadku
     */
    public boolean isIdentityMatrix() throws MatrixException {
        if (!isSquareMatrix()) {
            throw new MatrixException("Macierz nie jest kwadratowa");
        }
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                if ((i != j) && (array[i][j] != 0)) {
                    return false;
                } else if ((i == j) && (array[i][j] != 1)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Pobiera dane z kolumny
     *
     * @param index - index kolumny
     * @return - zwraca tablicê danych lub null jesli index jest poza
     * macierz¹
     */
    public double[] getCol(int index) {
        double[] tabl = null;
        if (isColIndexValid(index)) {
            tabl = new double[getRowCount()];
            for (int i = 0; i < tabl.length; i++) {
                tabl[i] = array[i][index];
            }
        }
        return tabl;
    }

    /**
     * Zwraca dane z rzêdu macierzy
     *
     * @param index - indeks rzêdu
     * @return - pobrane dane lub null jesli indeks rzêdu jest poza macierz¹
     */
    public double[] getRow(int index) {
        double[] tabl = null;
        if (isRowIndexValid(index)) {
            tabl = new double[getColCount()];
            for (int i = 0; i < tabl.length; i++) {
                tabl[i] = array[index][i];
            }
        }
        return tabl;
    }

    /**
     * Ustawia wartoci kolumny na wartoci z podanej tablicy
     *
     * @param index - indeks kolumny
     * @param col   - wartoci do wstawienia
     * @throws MatrixException
     */
    public void setCol(int index, double[] col) throws MatrixException {
        if (isColIndexValid(index)) {
            for (int i = 0; i < col.length; i++) {
                array[i][index] = col[i];
            }
        } else {
            throw new MatrixException("indeks kolumny poza macierz¹");
        }
    }

    /**
     * Zmienia wartoci q rzêdzie na podane
     *
     * @param index - index rzêdu
     * @param row   - dane do wstawienia
     * @throws MatrixException
     */
    public void setRow(int index, double[] row) throws MatrixException {
        if (isRowIndexValid(index)) {
            for (int i = 0; i < row.length; i++) {
                array[index][i] = row[i];
            }
        } else {
            throw new MatrixException("index rzêdu poza macierz¹");
        }
    }

    /**
     * Pobiera wartoæ umieszczon¹ w komórce
     *
     * @param row - rzad
     * @param col - kolumna
     * @return - wartoæ na przeciêciu rzêdu i koumny
     */
    public double getCell(int row, int col) throws MatrixException {
        double v;
        if (isIndexValid(row, col)) {
            v = array[row][col];
        } else {
            throw new MatrixException("indeks rzêdu lub kolumny poza macierz¹");
        }
        return v;
    }

    /**
     * Zmienia wartoæ komórki na podan¹
     *
     * @param row   - rz¹d
     * @param col   - kolumna
     * @param value - wartoæ do wstawienia
     */
    public void setCell(int row, int col, double value) throws MatrixException {
        if (isIndexValid(row, col)) {
            array[row][col] = value;
        } else {
            throw new MatrixException("indeks rzêdu lub kolumny poza macierz¹");
        }
    }

    /**
     * Sprawdza czy macierz jest elementarna, czyli czy ma dok³adnie
     * jeden element ró¿ny od zera
     *
     * @return true jesli jest elementarna, false w przeciwnym wypadku
     */
    public boolean isElementaryMatrix() {
        int sum = 0;
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                if (array[i][j] != 0.0) {
                    sum++;
                }
            }
        }
        return sum == 1;
    }

    /**
     * Sprawdza czy macierz jest wierszowa (jest wektorem wierszowym
     *
     * @return true jesli jest wierszowa, false w przeciwnym wypadku
     */
    public boolean isRowMatrix() {
        return getRowCount() == 1;
    }

    /**
     * Sprawdza czy macierz jest kolumnowa (jest wektorem kolumnowym
     *
     * @return true jesli jest kolumnowa, false w przeciwnym wypadku
     */
    public boolean isColumnMatrix() {
        return getColCount() == 1;
    }

    /**
     * Sprawdza czy macierz jest kwadratowa tzn. czy liczba rzêdów
     * i kolumn jest jednakowa
     *
     * @return true jesli jest kwadratowa, false w przeciwnym wypadku
     */
    public boolean isSquareMatrix() {
        return getRowCount() == getColCount();
    }

    public boolean is3x3() {
        int r = getRowCount();
        int c = getColCount();
        return (r == 3) && (c == 3);
    }

    /**
     * Sprawdza czy macierz jest diagonalna. Macierz jest diagonalna jeli
     * wszystkie elemnty s¹ zerowe z wyj¹tkiem elementów le¿¹cych na g³ównej
     * przek¹tnej
     *
     * @return true jesli jest diagonalna, false w przeciwnym wypadku
     */
    public boolean isDiagonalMatrix() throws MatrixException {
        if (!isSquareMatrix()) {
            throw new MatrixException("Macierz nie jest kwadratowa");
        }
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                if ((array[i][j] != 0.0) && (i != j)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Sprawdza czy macierz jest symetryczna. Symetryczna macierz to taka, w
     * której elementy po obu stronach przek¹tnej g³ównej s¹ równe
     *
     * @return true jeli jest symetryczna, false jeli nie jest
     * symetryczna
     * @throws MatrixException
     */
    public boolean isSymetricMatrix() throws MatrixException {
        if (!isSquareMatrix()) {
            throw new MatrixException("Macierz nie jest kwadratowa");
        }
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                if (array[i][j] != array[j][i]) {
                    return false;
                }
            }
        }
        return true;
    }

    public void showAsJTable() {
        JFrame frame = new JFrame();
        JTable table = new JTable();
        JScrollPane sp = new JScrollPane(table);
        MatrixTableModel mtm = new MatrixTableModel(array);
        table.setDefaultRenderer(Object.class, new MatrixReenderer());
        table.setModel(mtm);
        frame.add(sp);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setTitle("Matrix");
        frame.pack();
        frame.setVisible(true);
    }

    /**
     * Sprawdza czy macierz jest antysymetryczna. Antysymetryczna to taka
     * macierz,w której elementy symetrycznie roz³o¿one po obu stronach g³ównej
     * przek¹tnej maj¹ jednakowe wartoci, ale z przeciwnym znakiem,
     * np. 3 i -3. Elementy diagonalne musza byæ równe 0
     *
     * @return
     * @throws MatrixException
     */
    public boolean isAntisymetricMatrix() throws MatrixException {
        if (!isSquareMatrix()) {
            throw new MatrixException("Macierz nie jest kwadratowa");
        }
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                if (array[i][j] != -array[j][i]) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        final int row = in.readInt();
        final int col = in.readInt();
        double[][] objs = new double[row][col];
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                objs[i][j] = (Double) in.readObject();
            }
        }
        setArray(objs);
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        final int row = array.length;
        final int col = array[0].length;
        out.writeInt(row);
        out.writeInt(col);
        for (double[] doubles : array) {
            for (int j = 0; j < col; j++) {
                out.writeObject(doubles[j]);
            }
        }
    }

    /**
     * Sprawdza czy macierz jest trójdiagonalna. W macierzy trójdiagonalnej
     * niezerowe mog¹ byæ tylko elementy le¿ace na g³ównej przek¹tnej i dwóch
     * przek¹tnych z ni¹ sasiaduj¹cych.
     *
     * @return true jesli macierz jest trojdiagonalna, false
     * w przeciwnym wypadku
     * @throws MatrixException
     */
    public boolean is3DiagonalMatrix() throws MatrixException {
        if (!isSquareMatrix()) {
            throw new MatrixException("Macierz nie jest kwadratowa");
        }
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                if ((array[i][j] == 0.0) && !(Math.abs(i - j) > 1)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Sprawdza czy macierz jest górnotrojk¹tna. Macierz jest
     * gornotrojkatna wtedy, gdy elementy  le¿¹ce ponizej g³ównej
     * przek¹tnej sa równe 0.
     *
     * @return true jesli macierz jest gornotrojkatna, false
     * w przeciwnym wypadku
     * @throws MatrixException
     */
    public boolean isUpperTriangleMatrix() throws MatrixException {
        if (!isSquareMatrix()) {
            throw new MatrixException("Macierz nie jest kwadratowa");
        }
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                if ((i > j) && (array[i][j] != 0)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Sprawdza czy macierz jest dolnotrojkatna. Macierz jest dolno-
     * trojkatna, gdy wszystkie elementy le¿¹ce powyzej g³ównej
     * przek¹tnej s¹ równe 0;
     *
     * @return
     * @throws MatrixException
     */
    public boolean isLowerTriangleMatrix() throws MatrixException {
        if (!isSquareMatrix()) {
            throw new MatrixException("Macierz nie jest kwadratowa");
        }
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                if ((i < j) && (array[i][j] != 0)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Monozy te macierz przez skalar;
     *
     * @param skalar - skalar
     */
    public void mult(double skalar) {
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                array[i][j] *= skalar;
            }
        }
    }

    /**
     * Zwraca nowa macierz bedaca wynikiem pomnozenia tej
     * macierzy przez skalar
     *
     * @param skalar
     * @return
     */
    public Matrix mult2(double skalar) {
        Matrix m = null;
        try {
            m = this.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        m.mult(skalar);
        return m;
    }

    /**
     * Sprawdza czy macierze maja jednakowa wielkosc
     *
     * @param m - macierz do porownania
     * @return zwraca true jesli macierze maja te sama
     * wielkosc, false - w przeciwnym wypadku
     */
    public boolean hasSizeEqual(Matrix m) {
        return (this.getRowCount() != m.getRowCount())
                || (this.getColCount() != m.getColCount());
    }

    /**
     * Dodaje wskazana macierz do tej macierzy
     *
     * @param m - macierz do dodania
     * @throws MatrixException
     */
    public void add(Matrix m) throws MatrixException {
        if (hasSizeEqual(m)) {
            throw new MatrixException("macierze nierównej wielkoci");
        }
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                array[i][j] += m.getCell(i, j);
            }
        }
    }

    /**
     * Zwraca nowa macierz bedaca wynikiem dodania wskazanej
     * macierzy do tej macierzy
     *
     * @param m - macierz do odjecia
     * @throws MatrixException
     */
    public Matrix add2(Matrix m) throws MatrixException {
        if (hasSizeEqual(m)) {
            throw new MatrixException("macierze nierównej wielkoci");
        }
        Matrix n = null;
        try {
            n = this.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        n.add(m);
        return n;
    }

    /**
     * Odejmuje wskazan¹ macierz od tej macierzy
     *
     * @param m - macierz do odjecia
     * @throws MatrixException
     */
    public void sub(Matrix m) throws MatrixException {
        if (hasSizeEqual(m)) {
            throw new MatrixException("macierze nierównej wielkoci");
        }
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                array[i][j] -= m.getCell(i, j);
            }
        }
    }

    /**
     * Zwraca nowa macierz bedaca wynikiem odjecia wskazanej
     * macierzy od tej macieryz
     *
     * @param m -macierz do odjecia
     * @throws MatrixException
     */
    public Matrix sub2(Matrix m) throws MatrixException {
        if (hasSizeEqual(m)) {
            throw new MatrixException("macierze nierównej wielkoci");
        }
        Matrix n = null;
        try {
            n = this.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        n.sub(m);
        return n;
    }

    /**
     * Mnozy te macierz przez podana macierz
     *
     * @param n - macierz, przez ktora bedzie pomnozona ta macierz
     */
    public void multiply(Matrix n) {
        double[][] temp = new double[getRowCount()][n.getColCount()];
        for (int i = 0; i < getRowCount(); i++) {//2
            for (int j = 0; j < n.getColCount(); j++) {//2
                for (int k = 0; k < getColCount(); k++) {//3
                    try {
                        temp[i][j] += getCell(i, k) * n.getCell(k, j);
                    } catch (MatrixException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        this.setArray(temp);
    }

    /**
     * Mnozy te macierz przez podana macierz i zwraca nowa macierz
     *
     * @param n - macierz przez ktora nalezy pomnozyc te macierz
     * @return - nowa macierz bedaca wynikiem pomnozenia tej macierzy przez
     * podana macierz
     */
    public Matrix multiply2(Matrix n) {
        double[][] temp = new double[getRowCount()][n.getColCount()];
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < n.getColCount(); j++) {
                for (int k = 0; k < getColCount(); k++) {
                    try {
                        temp[i][j] += getCell(i, k) * n.getCell(k, j);
                    } catch (MatrixException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return new Matrix(temp);
    }

    //odwraca te macierz
    public void transpose() {
        double[][] temp = new double[getRowCount()][getColCount()];
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                temp[i][j] = array[j][i];
            }
        }
        setArray(temp);
    }

    //Odwraca matrix m i jej wartosci wstawia do tej macierzy
    public void transpose(Matrix m) {
        m.transpose();
        for (int i = 0; i < getRowCount(); i++) {
            for (int j = 0; j < getColCount(); j++) {
                array[i][j] = m.array[i][j];
            }
        }
    }

    public double det() {
        double det = 0.0;
        switch (getRowCount()) {
            case 1:
                if (isSquareMatrix()) {
                    det = array[0][0];
                }
                break;
            case 2:
                if (isSquareMatrix()) {
                    det = array[0][0] * array[1][1] - array[0][1] * array[1][0];
                }
                break;
            case 3:
                if (isSquareMatrix()) {
                    double a = array[0][0];
                    double b = array[0][1];
                    double e = array[0][2];
                    double c = array[1][0];
                    double d = array[1][1];
                    double f = array[1][2];
                    double g = array[2][0];
                    double h = array[2][1];
                    double i = array[2][2];
                    det = ((a * d * i + b * f * g + e * c * h) - (e * d * g + a
                            * f * h + b * c * i));
                }
                break;
            default:
                break;
        }
        return det;
    }

    public void setToTranslate(double dx, double dy) {
        this.array[0][2] = dx;
        this.array[1][2] = dy;
    }

    public void setToScale(double sx, double sy) {
        this.array[0][0] = sx;
        this.array[1][1] = sy;
    }

    public void setToRotation(double angleDeg) {
        double angleRad = MathUtil.degToRad(angleDeg);
        double t1 = Math.cos(angleRad);
        double t2 = Math.sin(angleRad);
        this.array[0][0] = t1;
        this.array[0][1] = t2;
        this.array[1][0] = -t2;
        this.array[1][1] = t1;
    }

    public void setToReflection(Line line) {
        double slope = line.slope();
        double angle = Math.atan(slope);
        double dangle = 2 * angle;
        double cosa = Math.cos(dangle);
        double sina = Math.sin(dangle);
        this.array[0][0] = cosa;
        this.array[0][1] = sina;
        this.array[1][0] = sina;
        this.array[1][1] = -cosa;
    }

    public void setToShear(double shx, double shy) {
        this.array[0][1] = shx;
        this.array[1][0] = shy;
    }

}
