from typing import Iterator, List, Mapping, Sequence, Set, Tuple, TypeVar, Union
from LinAlgebraPy.vectors.vector import Vector, colinear, indexPivot, indexPivotColumn
from LinAlgebraPy.matrices.matrixfunc import onlytypeM, sortList, sortMatrix
from math import sqrt


def Identity(n: int):
    """Identity Matrix

    Args:
        n (integer): Dimension of the square matrix

    Returns:
        [Matrix]: Identity Matrix (n x n)
    """
    lis = []
    elem = []
    for i in range(n):
        for j in range(n):
            if i != j:
                lis.append(0)
            else:
                lis.append(1)
        elem.append(lis)
        lis = []
    return Matrix(elem)


class Matrix():
    """Matrix

        Args:
            This Matrix can take only one type of arguments:
            -2 integers : it will create a zero Matrix with dimensions (i,j)
            -Vectors (Column or Line): Vectors should have the same dimension and same type of Vectors
            -List of vectors : Vectors in the List should have the same dimension and same type of Vectors
            -Tuples: Tuples should have the same dimensions and contain only numbers (int,float,complex)
            -List (only numbers) : It will add every list as a line in the Matrix and should have the same length
            -List of lists:  List of lists (only numbers) as seen before

        To make some changes to the Matrix (set):
            Two types of changes:
            (i index of line and j index of column)
            1)  To change an entry:
                Example:
                    M[i,j]=2  (It will set 2 to the entry  at i line and j column)

            2) To change a line or a column:
                Example for changing a line:
                    M['L',i]=Vector   (Vector should be a line and have the same dimensions of the lines in the Matrix)
                Example for changing a vector:
                    M['C',j]=Vector   (Vector should be a column and have the same dimensions of the columns in the Matrix)

        To get an entry or a Vector from a Matrix (get):
            Two types of get:
            1) M[i,j]    ,it will return an element of type int or float or complex

            2) M['L',i]  ,it will return a the Line in j position as a Vector Line
               M['C',j] , it will return the Column in j position as a Vector Column

    """

    def __init__(self, *args: Union[int, Vector, List[Vector], List[Union[int, float, complex]], List[List[Union[int, float, complex]]], Tuple[Union[int, float, complex]]]):
        elementsM = []
        onlylist = onlytypeM(args, list)
        onlyvector = onlytypeM(args, Vector)
        onlytuple = onlytypeM(args, tuple)
        types = [float, int, complex, list, tuple, Vector]
        length = len(args)
        i = 0

        if len(args) == 2 and type(args[0]) == int and type(args[1]) == int:
            (m, n) = args
            new = []
            i, j = 0, 0
            while i < m:
                while j < n:
                    new.append(0)
                    j += 1
                elementsM.append(new)
                new = []
                i += 1
                j = 0
        elif onlylist:
            if type(args[0][0]) in types[0: 3]:
                while i < length:
                    c = args[i]
                    elementsM.append(c)
                    i += 1
            elif type(args[0][0]) == list:
                while i < length:
                    c = args[i]
                    elementsM += c
                    i += 1
            elif type(args[0][0]) == Vector:
                ArgsIn = args[0]
                onlyVectorIn = onlytypeM(ArgsIn, Vector)
                len2 = len(ArgsIn)
                if onlyVectorIn == len2 and onlyVectorIn != 0:
                    while i < len2:
                        c = ArgsIn[i]
                        elementsM.append(c.elements)
                        i += 1
                elif onlyVectorIn == sqrt(2)*len2 and onlyVectorIn != 0:
                    (m, n) = ArgsIn[0].dim
                    while i < m:
                        elementsM += [[j.elements[i][0] for j in ArgsIn]]
                        i += 1
            else:
                raise ValueError(
                    'Type of Arguments allowed are in this list => [float,int,complex,list,Vector]')
        elif onlytuple:
            while i < length:
                c = args[i]
                elementsM += [list(c)]
                i += 1
        elif onlyvector == length and onlyvector != 0:
            while i < length:  # Problem in Vectors Column
                c = args[i]
                elementsM += [c.elements]
                i += 1
        elif onlyvector == sqrt(2)*length and onlyvector != 0:
            (m, n) = args[0].dim
            while i < m:
                elementsM += [[j.elements[i][0] for j in args]]
                i += 1
        else:
            raise TypeError("Arguments allowed are list, tuple and Vector ")
        self.elements = elementsM
        self.dim = (len(self.elements), len(self.elements[0]))
        for i in range(self.dim[0]):
            for j in range(self.dim[1]):
                elem = elementsM[i][j]
                if type(elem) in types[0:3]:
                    instance = type(elem)
                    if instance != complex and elem <= 1e-13 and elem >= -1e-13:  # changed type in echelon with N and Echelon
                        elementsM[i][j] = 0
                    elif instance == complex:
                        real = complex(elementsM[i][j]).real
                        imag = complex(elementsM[i][j]).imag
                        comparereal = (real <= 1e-13 and real >= -1e-13)
                        compareimag = (imag <= 1e-13 and imag >= -1e-13)
                        if comparereal and compareimag:
                            elementsM[i][j] = 0
                        elif compareimag:
                            elementsM[i][j] = round(real, 14)
                        elif comparereal:
                            elementsM[i][j] = round(imag, 14)*1j
                else:
                    raise ValueError(
                        "1-Arguments allowed are list, tuple , int, complex, float and Vector \n 2-You can't combine Vector , tuple and list together . It should be unique")

        self.isSquared = self.dim[0] == self.dim[1]

    def __add__(self, mat):
        """Adding a Matrix

        Args:
            mat (Matrix): The two added Matrix should have the same Dimensions

        Raises:
            ValueError: A matrix can only be added to another matrix if the two matrices have the same dimensions .

        Returns:
            [Matrix]: Addition of the Matrix
        """
        elements = self.elements
        if type(mat) == Matrix and mat.dim == self.dim:
            elemA = []
            lis = []
            for i in range(len(elements)):
                for j in range(len(elements[0])):
                    lis.append(elements[i][j]+mat.elements[i][j])
                elemA.append(lis)
                lis = []
            return Matrix(elemA)
        else:
            raise ValueError(
                "A matrix can only be added to another matrix if the two matrices have the same dimensions .")

    def __radd__(self, mat):
        return self.__add__(mat)

    def __IADD__(self, mat):
        return self.__add__(mat)

    def __sub__(self, mat):
        """Substitution

        Args:
            mat (Matrix): The two matrices should have the same dimensions

        Raises:
            ValueError: A matrix can only be added to another matrix if the two matrices have the same dimensions .

        Returns:
            [Matrix]: Substitution Matrix
        """
        elements = self.elements
        if type(mat) == Matrix and mat.dim == self.dim:
            elemA = []
            lis = []
            for i in range(len(elements)):
                for j in range(len(elements[0])):
                    lis.append(elements[i][j]-mat.elements[i][j])
                elemA.append(lis)
                lis = []
            return Matrix(elemA)
        else:
            raise ValueError(
                "A matrix can only be added to another matrix if the two matrices have the same dimensions .")

    def __ISUB__(self, mat):
        return self.__sub__(mat)

    def __rsubb__(self, mat):
        return self.__sub__(mat)

    def __mul__(self, var):
        """Multiplying a Matrix by a scalar or another Matrix or a Vector

        Args:
            var (Matrix||Vector||integer||float||complex): 
            -Multiplying a Matrix with a scalar will result to 
            multiplying every element in the Matrix
            -Multiplying A Matrix with another Matrix
            -Multiplyinf A matrix with a Vector

        Raises:
            ValueError: Multiplying two matrices or a matrix by a vector cannot be done if n1!=m2 ( dim(A)=(m1,n1) dim(B)=(m2,n2)  )

        Returns:
            Matrix 
        """
        if type(var) == Vector:
            var = Matrix(var)
        if type(var) == Matrix:
            (m1, n1) = self.dim
            (m2, n2) = var.dim
            if n1 == m2:
                elements = self.elements
                lis, elemA = [], []
                res = 0
                for i in range(m1):
                    for j in range(n2):
                        for k in range(n1):
                            res = res + elements[i][k]*var.elements[k][j]
                        lis.append(res)
                        res = 0
                    elemA.append(lis)
                    lis = []
                return Matrix(elemA)
            else:
                raise ValueError(
                    "Multiplying two matrices or a matrix by a vector cannot be done if n1!=m2 ( dim(A)=(m1,n1) dim(B)=(m2,n2)  )")
        else:
            elements = self.elements
            elemA, lis = [], []
            for i in range(len(elements)):
                for j in range(len(elements[0])):
                    lis.append(elements[i][j]*var)
                elemA.append(lis)
                lis = []
            return Matrix(elemA)

    def __rmul__(self, var):
        self.__mul__(var)

    def __IMUL__(self, var):
        self.__mul__(var)

    def __getitem__(self, index):
        i, j = index
        if type(i) == int:  # Format M[i,j]
            return self.elements[i][j]
        elif i == "L":  # Format M["L",j] (returs a Vector at j index)
            return Vector(self.elements[j])
        elif i == "C":  # Format M["C",j] (returs a Vector at j index)
            m = self.transpose()
            return Vector(m.elements[j]).transpose()
        else:
            raise ValueError()

    def __setitem__(self, index, val):
        (i, j) = index
        dimension = self.dim
        instance = type(val)
        # changed type in echelon with N and Echelon
        if instance != complex and instance != Vector and val <= 1e-14 and val >= -1e-14:
            val = 0
        elif instance == complex:
            real = complex(val).real
            imag = complex(val).imag
            comparereal = (real <= 1e-13 and real >= -1e-13)
            compareimag = (imag <= 1e-13 and imag >= -1e-13)
            if comparereal and compareimag:
                val = 0
            elif compareimag:
                val = round(real, 14)
            elif comparereal:
                val = round(imag, 14)*1j

        if type(i) == int:
            # Format M[i,j]= integer
            self.elements[i][j] = val
        elif i == "L" and type(val) == Vector and val.isLine:
            # Format M["L",j]=Vector(..) (Vector should be a Line)
            self.elements[j] = val.elements
        elif i == "C" and type(val) == Vector and val.isColumn:
            # Format M["C",j]=Vector(..) (Vector should be a Column)
            (m, n) = val.dim
            for k in range(m):
                self.elements[k][j] = val.elements[k][0]

    def trace(self):
        i = 0
        res = 0
        len = self.dim[0]
        if self.isSquared:
            while i < len:
                res += self.elements[i][i]
                i += 1
            return res
        else:
            raise ValueError("Matrix should be square")

    def transpose(self):
        (m, n) = self.dim
        new = Matrix(n, m).elements
        i, j = 0, 0
        while i < m:
            while j < n:
                new[j][i] = self.elements[i][j]
                j += 1
            j = 0
            i += 1
        return Matrix(new)

    def echelon(self, choice=None):
        """Echelon a Matrix
        This function has a float precision to 1e-14 and 1e14 for better performance
        Args:
            choice (string, optional): [description]. Defaults to None.

        Returns:
            Matrix: Echeloned Matrix (REF or RREF)
        """
        (m, n) = self.dim
        Vect = [self["L", i] for i in range(m)]
        index = [indexPivot(x) for x in Vect]
        (index, Vect, k, negative) = sortList(index, Vect)

        L = []
        for i in range(len(index)-k):
            (index, Vect, k, neg) = sortList(index, Vect)
            negative *= neg

            ind = index[i]

            for j in range(i+1, len(index)-k):
                if index[j] in L:
                    break
                if index[i] == index[j]:
                    Vect[j] = Vect[j] - Vect[i]*(Vect[j][ind]/Vect[i][ind])
            L.append(ind)
            index = [indexPivot(x) for x in Vect]

        if choice == "DET":
            return (Matrix(Vect), negative)

        index = [indexPivot(x) for x in Vect]
        for i in range(len(index)):
            if index[i] == -1:
                break
            else:
                Vect[i] = Vect[i]*(1/Vect[i][index[i]])

        if choice == None or choice == "REF":
            return Matrix(Vect)

        for j in range(len(index)):
            for i in range(j):
                if i == -1 or Vect[i].isNul():
                    break
                else:
                    Vect[i] = Vect[i]-(Vect[j]*Vect[i][index[j]])

        if choice == "RREF":
            return Matrix(Vect)

    def det(self):
        if self.isSquared:
            (mat, neg) = self.echelon(choice="DET")

            (n, m) = mat.dim
            res = 1
            for i in range(n):
                res *= mat.elements[i][i]

            return res*neg
        else:
            raise ValueError("Matrix is not a square matrix")

    def isInversible(self) -> bool:
        try:
            if self.det() != 0:
                return True
        except:
            return False

    def __repr__(self):
        string = ''
        i, j = 0, 0
        elem = self.elements
        while i < len(elem):
            string += '('
            while j < len(elem[0]):
                e = elem[i][j]
                if type(e) != complex:
                    e = round(e, 4)
                string += " {:^g} ".format(e)
                j += 1
            string += ') \n'
            j = 0
            i += 1
        return string

    def __eq__(self, other):

        (n1, m1) = self.dim
        (n2, m2) = other.dim
        if self.dim != other.dim:
            raise ValueError(
                "Cannot compare two matrices with different dimensions")
        first = self.elements
        second = other.elements
        for i in range(m1):
            for j in range(n1):
                if first[i][j] != second[i][j]:
                    return False
        return True

    def echelonWithN(self, N):
        (m, n) = self.dim
        (m1, n1) = N.dim
        if type(N) == Vector and m == m1:
            N = Matrix(N)
        elif type(N) != Matrix and m1 != m:
            raise ValueError(
                "N should be a matrix or a vector column and the same dimension")

        Nvect = [N["L", i] for i in range(m)]
        Vect = [self["L", i] for i in range(m)]
        index = [indexPivot(x) for x in Vect]
        (index, Vect, k, negative, Nvect) = sortMatrix(index, Vect, Nvect)
        L = []

        for i in range(len(index)-k):
            (index, Vect, k, neg, Nvect) = sortMatrix(index, Vect, Nvect)
            negative *= neg
            ind = index[i]
            if index[i] == -1:
                break
            else:
                # here there was a problem on 06/09/2021
                Nvect[i] = Nvect[i]*(1/Vect[i][ind])
                Vect[i] = Vect[i]*(1/Vect[i][ind])
            for j in range(i+1, len(index)-k):

                if index[j] in L:
                    break
                if index[i] == index[j]:
                    Nvect[j] = Nvect[j] - Nvect[i]*(Vect[j][ind])
                    Vect[j] = Vect[j] - Vect[i]*(Vect[j][ind])

            L.append(ind)
            index = [indexPivot(x) for x in Vect]

        for j in range(len(index)):
            for i in range(j):
                if i == -1 or Vect[i].isNul():
                    break
                else:
                    Nvect[i] = Nvect[i]-(Nvect[j]*Vect[i][index[j]])
                    Vect[i] = Vect[i]-(Vect[j]*Vect[i][index[j]])

        if n1 == 1:
            return Matrix(Nvect).transpose()
        else:
            return Matrix(Nvect)

    def isSymmetric(self) -> bool:
        if self.isSquared:
            (m, n) = self.dim
            i = 0
            while i < m:
                if self["L", i] != self["C", i].transpose():
                    return False
                i += 1
            return True
        else:
            raise ValueError("The matrix should be squared ")

    def __invert__(self):
        """Inverse a Matrix

        Raises:
            ValueError: The matrix is not inversible (If it's a square matrix, det=0 or the matrix is not square

        Returns:
            Matrix: The inverse of a Matrix
        """
        if self.isInversible():
            (m, n) = self.dim
            I = Identity(m)
            return self.echelonWithN(I)
        else:
            raise ValueError(
                "The matrix is not inversible (If it's a square matrix, det=0 or the matrix is not square")

    def rank(self) -> int:
        (m, n) = self.dim
        matecheloned = self.echelon(choice="RREF")
        Vect = [matecheloned["L", i] for i in range(m)]
        index = [indexPivot(x) for x in Vect]
        try:
            rank = index.index(-1)
        except:
            rank = n
        return rank

    def _DiagonalMatrix(self) -> bool:
        if self.isSquared:
            (m, n) = self.dim
            elem = self.elements
            for i in range(m):
                for j in range(n):
                    if i != j and elem[i][j] != 0:
                        return False
            return True
        else:
            raise ValueError("The matrix should be squared")

    def _expDiagonalMatrix(self, n):
        if self._DiagonalMatrix():
            (m1, n1) = self.dim
            for i in range(m1):
                if n < 0 and self.elements[i][i] == 0:
                    raise ValueError(
                        'The matrix is not Inversible and can not accept negative exponents \n Reminder : if an eigenvalue is equal to 0, the matrix is not inversible ')
                else:
                    self.elements[i][i] **= n

            return Matrix(self.elements)
        else:
            raise ValueError('Matrix should be squared and diagonal')

    def Null(self, sub=None):
        """Null Space

        Args:
            sub (int||float||complex, optional): used mostly for finding Eigenvectors.
            Example: (Ker(Matrix - sub*Identity)
            Defaults to None.

        Returns:
            array: Null space vectors
        """
        (m, n) = self.dim
        rank = 0
        iden = Identity(n)
        if self.isSquared and sub != None:
            matEcheloned = self - (iden*sub)
            matEcheloned = matEcheloned.echelon("RREF")

        else:
            matEcheloned = self.echelon("RREF")

        Vect = [matEcheloned["C", i] for i in range(n)]
        index = [indexPivotColumn(x) for x in Vect]
        idenVectors = [iden["C", i] for i in range(n)]
        VectNull = Vector([[0] for x in range(n)])
        NullSpace = []
        indexCroissant = []
        max = index[0]
        if max == -1:
            NullSpace.append(idenVectors[0])
        for i in range(1, n):

            if index[i] == max and index[i] != -1 or index[i] in indexCroissant:
                vect1 = idenVectors[i]
                for j in range(m):
                    vect1[j] -= Vect[i][j]
                NullSpace.append(vect1)
            elif index[i] == -1:
                NullSpace.append(idenVectors[i])
            indexCroissant.append(max)
            max = index[i]
        if len(NullSpace) == 0:
            NullSpace.append(VectNull)
        return NullSpace

    def eigenvalues(self):
        """Eigenvalues for 2x2 Matrix

        Raises:
            ValueError: "Can't compute eigenvalues with complex determinant yet! :("
            ValueError: The Matrix isn't Squared

        Returns:
            List: list of eigenvalues
        """
        if self.isSquared:
            (m, n) = self.dim
            trace = -self.trace()
            det = self.det()
            delta = 0
            eigenV = []
            Vect = [self["L", i] for i in range(m)]
            index = [indexPivot(x) for x in Vect]
            # (L, Vect, k, neg) = sortList(index, Vect)
            # det *= neg
            if m == 2 and n == 2:
                delta = ((trace**2)-4*det)
                if type(delta) != complex:
                    if delta < 0:
                        eigenV.extend(
                            [(-trace-1j*sqrt(-delta))/2, (-trace+1j*sqrt(-delta))/2])
                    elif delta > 0:

                        eigenV.extend(
                            [(-trace-sqrt(delta))/2, (-trace+sqrt(delta))/2])
                    else:
                        eigenV.append(-trace/2)
                else:
                    raise ValueError(
                        "Can't compute eigenvalues with complex determinant yet! :(")
            return eigenV

        else:
            raise ValueError("The Matrix isn't Squared")

    def Diagonalizable(self):
        """Diagonalizable

        Raises:
            ValueError: The matrix should be squared

        Returns:
            Bool: if the matrix is diagonalizable
        """
        if self.isSquared:
            eigen = self.eigenvalues()
            multiplicity = {i: eigen.count(i) for i in eigen}
            for i in range(len(eigen)):
                null = self.Null(sub=eigen[i])
                if(len(null) != multiplicity[eigen[i]]):
                    return False
            return True
        else:
            raise ValueError('The matrix should be squared')

    def __pow__(self, var):
        """Power of a Matrix

        Args:
            var (int): the var is power chosen for the matrix

        Raises:
            ValueError: This operation only works for 2x2 real matrix for now

        Returns:
            Matrix: Matrix to the power of var
        """
        (m, n) = self.dim
        if m == 2 and n == 2:
            if(self.Diagonalizable()):
                (m, n) = self.dim
                diagonal = Identity(m)
                eigenValues = self.eigenvalues()
                null = []
                for i in range(m):
                    diagonal[i, i] *= eigenValues[i]
                for i in range(len(eigenValues)):
                    null += self.Null(sub=eigenValues[i])

                eigenVectorsMatrix = Matrix(null)
                inverseEigenVectorsMatrix = eigenVectorsMatrix.__invert__()
                return eigenVectorsMatrix*diagonal._expDiagonalMatrix(var)*inverseEigenVectorsMatrix
        else:
            raise ValueError(
                "This operation only works for 2x2 real matrix for now")

    def __ipow__(self, var):
        return self.__pow__(var)


# def GaussElimination(ma,v1) -> Matrix:
#    return ma.echelonWithN(v1)


def CramerRule(ma, v1) -> Vector:
    (m, n) = ma.dim
    ele = []
    determ = ma.det()
    if determ != 0:
        for i in range(n):
            m1 = Matrix(ma.elements)
            m1["C", i] = v1
            ele.append([m1.det()/determ])
    return Vector(ele)
