"""
Pure python inversion of small matrices, to avoid requiring numpy or similar in SimTK.
"""

import sys

def eye(size):
    """
    Returns identity matrix.
    
    >>> print(eye(3))
    [[1, 0, 0]
     [0, 1, 0]
     [0, 0, 1]]
    """
    result = []
    for row in range(0, size):
        r = []
        for col in range(0, size):
            if row == col:
                r.append(1)
            else:
                r.append(0)
        result.append(r)
    return MyMatrix(result)
    
def zeros(m, n=None):
    """
    Returns matrix of zeroes
    
    >>> print(zeros(3))
    [[0, 0, 0]
     [0, 0, 0]
     [0, 0, 0]]
    """
    if n == None:
        n = m
    result = []
    for row in range(0, m):
        r = []
        for col in range(0, n):
            r.append(0)
        result.append(r)
    return MyMatrix(result)

class MyVector(object):
    """
    Parent class of MyMatrix and type of Matrix Row.
    """
    def __init__(self, collection):
        if isinstance(collection, MyVector):
            self.data = collection.data
        else:
            self.data = collection

    def __str__(self):
        return str(self.data)
        
    def __repr__(self):
        return self.__class__.__name__ + "(" + repr(self.data) + ")"

    def __getitem__(self, key):
        return self.data[key]

    def __contains__(self, item):
        return item in self.data
        
    def __delitem__(self, key):
        del self.data[key]
        
    def __iter__(self):
        for item in self.data:
            yield item

    def __len__(self):
        return len(self.data)
    
    def __setitem__(self, key, value):
        self.data[key] = value
        
    def __rmul__(self, lhs):
        try:
            len(lhs)
            # left side is not scalar, delegate mul to that class
            return NotImplemented
        except TypeError:
            new_vec = []
            for element in self:
                new_vec.append(lhs * element)
            return self.__class__(new_vec)

class MyMatrix(MyVector):
    """
    Pure python linear algebra matrix for internal matrix inversion in UnitSystem.
    
    >>> m = MyMatrix([[1,0,],[0,1,]])
    >>> print(m)
    [[1, 0]
     [0, 1]]
    >>> print(~m)
    [[1.0, 0.0]
     [0.0, 1.0]]
    >>> print(eye(5))
    [[1, 0, 0, 0, 0]
     [0, 1, 0, 0, 0]
     [0, 0, 1, 0, 0]
     [0, 0, 0, 1, 0]
     [0, 0, 0, 0, 1]]
    >>> m = eye(5)
    >>> m[1][1]
    1
    >>> m[1:4]
    MyMatrixTranspose([[0, 0, 0],[1, 0, 0],[0, 1, 0],[0, 0, 1],[0, 0, 0]])
    >>> print(m[1:4])
    [[0, 0, 0]
     [1, 0, 0]
     [0, 1, 0]
     [0, 0, 1]
     [0, 0, 0]]
    >>> print(m[1:4][0:2])
    [[0, 1]
     [0, 0]
     [0, 0]]
    >>> m[1:4][0:2] = [[9,8],[7,6],[5,4]]
    >>> print(m)
    [[1, 0, 0, 0, 0]
     [9, 8, 0, 0, 0]
     [7, 6, 1, 0, 0]
     [5, 4, 0, 1, 0]
     [0, 0, 0, 0, 1]]
    """
    def numRows(self):
        return len(self.data)
        
    def numCols(self):
        if len(self.data) == 0:
            return 0
        else:
            return len(self.data[0])

    def __len__(self):
        return self.numRows()

    def __str__(self):
        result = ""
        start_char = "["
        for m in range(0, self.numRows()):
            result += start_char
            result += str(self[m])
            if m < self.numRows() - 1:
                result += "\n"
            start_char = " "
        result += "]"
        return result
        
    def __repr__(self):
        return 'MyMatrix(' + MyVector.__repr__(self) + ')'

    def is_square(self):
        return self.numRows() == self.numCols()
    
    def __iter__(self):
        for item in self.data:
            yield MyVector(item)

    def __getitem__(self, m):
        if isinstance(m, slice):
            return MyMatrixTranspose(self.data[m])
        else:
            return MyVector(self.data[m])

    def __setitem__(self, key, rhs):
        if isinstance(key, slice):
            self.data[key] = rhs
        else:
            assert len(rhs) == self.numCols()
            self.data[key] = MyVector(rhs)

    def __mul__(self, rhs):
        """
        Matrix multiplication.
        
        >>> a = MyMatrix([[1,2],[3,4]])
        >>> b = MyMatrix([[5,6],[7,8]])
        >>> print(a)
        [[1, 2]
         [3, 4]]
        >>> print(b)
        [[5, 6]
         [7, 8]]
        >>> print(a*b)
        [[19, 22]
         [43, 50]]
        
        """
        m = self.numRows()
        n = len(rhs[0])
        r = len(rhs)
        if self.numCols() != r:
            raise ArithmeticError("Matrix multplication size mismatch (%d vs %d)" % (self.numCols(), r))
        result = zeros(m, n)
        for i in range(0, m):
            for j in range(0, n):
                for k in range(0, r):
                    result[i][j] += self[i][k]*rhs[k][j]
        return result

    def __add__(self, rhs):
        """
        Matrix addition.
        
        >>> print(MyMatrix([[1, 2],[3, 4]]) + MyMatrix([[5, 6],[7, 8]]))
        [[6, 8]
         [10, 12]]
        """
        m = self.numRows()
        n = self.numCols()
        assert len(rhs) == m
        assert len(rhs[0]) == n
        result = zeros(m,n)
        for i in range(0,m):
            for j in range(0,n):
                result[i][j] = self[i][j] + rhs[i][j]
        return result

    def __sub__(self, rhs):
        """
        Matrix subtraction.
        
        >>> print(MyMatrix([[1, 2],[3, 4]]) - MyMatrix([[5, 6],[7, 8]]))
        [[-4, -4]
         [-4, -4]]
        """
        m = self.numRows()
        n = self.numCols()
        assert len(rhs) == m
        assert len(rhs[0]) == n
        result = zeros(m,n)
        for i in range(0,m):
            for j in range(0,n):
                result[i][j] = self[i][j] - rhs[i][j]
        return result

    def __pos__(self):
        return self
        
    def __neg__(self):
        m = self.numRows()
        n = self.numCols()
        result = zeros(m, n)
        for i in range(0,m):
            for j in range(0,n):
                result[i][j] = -self[i][j]
        return result

    def __invert__(self):
        """
        >>> m = MyMatrix([[1,1],[0,1]])
        >>> print(m)
        [[1, 1]
         [0, 1]]
        >>> print(~m)
        [[1.0, -1.0]
         [0.0, 1.0]]
        >>> print(m*~m)
        [[1.0, 0.0]
         [0.0, 1.0]]
        >>> print(~m*m)
        [[1.0, 0.0]
         [0.0, 1.0]]
        >>> m = MyMatrix([[1,0,0],[0,0,1],[0,-1,0]])
        >>> print(m)
        [[1, 0, 0]
         [0, 0, 1]
         [0, -1, 0]]
        >>> print(~m)
        [[1.0, 0.0, 0.0]
         [0.0, 0.0, -1.0]
         [0.0, 1.0, 0.0]]
        >>> print(m*~m)
        [[1.0, 0.0, 0.0]
         [0.0, 1.0, 0.0]
         [0.0, 0.0, 1.0]]
        >>> print(~m*m)
        [[1.0, 0.0, 0.0]
         [0.0, 1.0, 0.0]
         [0.0, 0.0, 1.0]]
        """
        assert self.is_square()
        if self.numRows() == 0:
            return self
        elif self.numRows() == 1:
            val = self[0][0]
            val = 1.0/val
            return MyMatrix([[val]])
        elif self.numRows() == 2: # 2x2 is analytic
            # http://en.wikipedia.org/wiki/Invertible_matrix#Inversion_of_2.C3.972_matrices
            a = self[0][0]
            b = self[0][1]
            c = self[1][0]
            d = self[1][1]
            determinant = a*d - b*c
            if determinant == 0:
                raise ArithmeticError("Cannot invert 2x2 matrix with zero determinant")
            else:
                return 1.0/(a*d - b*c) * MyMatrix([[d, -b],[-c, a]])
        else:
            # Gauss Jordan elimination from numerical recipes
            n = self.numRows()
            m1 = self.numCols()
            assert n == m1
            # Copy initial matrix into result matrix
            a = zeros(n, n)
            for i in range (0,n):
                for j in range (0,n):
                    a[i][j] = self[i][j]
            # These arrays are used for bookkeeping on the pivoting
            indxc = [0] * n
            indxr = [0] * n
            ipiv = [0] * n
            for i in range (0,n):
                big = 0.0
                for j in range (0,n):
                    if ipiv[j] != 1:
                        for k in range (0,n):
                            if ipiv[k] == 0:
                                if abs(a[j][k]) >= big:
                                    big = abs(a[j][k])
                                    irow = j
                                    icol = k
                ipiv[icol] += 1
                # We now have the pivot element, so we interchange rows...
                if irow != icol:
                    for l in range(0,n):
                        temp = a[irow][l]
                        a[irow][l] = a[icol][l]
                        a[icol][l] = temp
                indxr[i] = irow
                indxc[i] = icol
                if a[icol][icol] == 0:
                    raise ArithmeticError("Cannot invert singular matrix")
                pivinv = 1.0/a[icol][icol]
                a[icol][icol] = 1.0
                for l in range(0,n):
                    a[icol][l] *= pivinv
                for ll in range(0,n): # next we reduce the rows
                    if ll == icol:
                        continue # except the pivot one, of course
                    dum = a[ll][icol]
                    a[ll][icol] = 0.0
                    for l in range(0,n):
                        a[ll][l] -= a[icol][l]*dum
            # Unscramble the permuted columns
            for l in range(n-1, -1, -1):
                if indxr[l] == indxc[l]:
                    continue
                for k in range(0,n):
                    temp = a[k][indxr[l]]
                    a[k][indxr[l]] = a[k][indxc[l]]
                    a[k][indxc[l]] = temp                
            return a

    def transpose(self):
        return MyMatrixTranspose(self.data)


class MyMatrixTranspose(MyMatrix):

    def transpose(self):
        return MyMatrix(self.data)
        
    def numRows(self):
        if len(self.data) == 0:
            return 0
        else:
            return len(self.data[0])
        
    def numCols(self):
        return len(self.data)

    def __getitem__(self, key):
        result = []
        for row in self.data:
            result.append(row[key])
        if isinstance(key, slice):
            return MyMatrix(result)
        else:
            return MyVector(result)

    def __setitem__(self, key, rhs):
        for n in range(0, len(self.data)):
            self.data[n][key] = rhs[n]

    def __str__(self):
        if len(self.data) == 0:
            return "[[]]"
        start_char = "["
        result = ""
        for m in range(0, len(self.data[0])):
            result += start_char
            result += "["
            sep_char = ""
            for n in range(0, len(self.data)):
                result += sep_char
                result += str(self.data[n][m])
                sep_char = ", "
            result += "]"
            if m < len(self.data[0]) - 1:
                result += "\n"
            start_char = " "
        result += "]"
        return result

    def __repr__(self):
        if len(self.data) == 0:
            return "MyMatrixTranspose([[]])"
        start_char = "["
        result = 'MyMatrixTranspose('
        for m in range(0, len(self.data[0])):
            result += start_char
            result += "["
            sep_char = ""
            for n in range(0, len(self.data)):
                result += sep_char
                result += repr(self.data[n][m])
                sep_char = ", "
            result += "]"
            if m < len(self.data[0]) - 1:
                result += ","
            start_char = ""
        result += '])'
        return result


# run module directly for testing
if __name__=='__main__':
    
    # Test the examples in the docstrings
    import doctest, sys
    doctest.testmod(sys.modules[__name__])
