from .vector import Vector


class MatrixDimensionError(BaseException):
    pass


class Matrix:
    def __init__(self, *rows):
        """
        Initialize Matrix object.

        Example:
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9])
        => <matrix_vector.matrix.Matrix object>

        Example:
        >> Matrix(Vector(1, 2, 3), Vector(4, 5, 6), Vector(7, 8, 9))
        => <matrix_vector.matrix.Matrix object>

        Arguments:
        N sequences or N vectors of the same size
        """
        if len(set(len(_) for _ in rows)) <= 1:
            self.elements = [Vector(*row) for row in rows]
        else:
            raise TypeError("All rows must be the same length")

    def rows(self):
        """
        Returns the number of rows of the matrix.

        Example:
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]).rows()
        => 3

        Arguments:
        No arguments
        """
        return len(self.elements)

    def colums(self):
        """
        Returns the number of colums of the matrix.

        Example:
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]).colums()
        => 3

        Arguments:
        No arguments
        """
        return self[0].size

    def get_colum(self, number):
        """
        Returns the n-th colum of the matrix as an object of class Vector.

        Example:
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]).get_colum(1)
        => Vector(2, 5, 8)

        Arguments:
        number : (int)
        """
        if (number in range(self.colums())):
            return Vector(*[element[number] for element in self.elements])
        else:
            raise IndexError("Matix index out of range")

    def get_row(self, number):
        """
        Returns the n-th row of the matrix as an object of class Vector.

        Example:
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]).get_row(1)
        => Vector(4, 5, 6)

        Arguments:
        number : (int)
        """
        if (number in range(self.rows())):
            return self[number]
        else:
            raise IndexError("Matix index out of range")

    def is_same_dimension(self, matrix):
        """
        Boolean method checkig if two matrices have the same dimensions.

        Example:
        >> Matrix([1, 2], [4, 5]).is_same_dimension(Matrix([3, 2], [6, 7]))
        => True

        Arguments:
        matrix : (Matrix)
        """
        rows = (self.rows() == matrix.rows())
        cols = (self.colums() == matrix.colums())
        return rows and cols

    def __add_matrix(self, matrix):
        return Matrix(*[list(x + y) for x, y in
                      zip(self.elements, matrix.elements)])

    def __add_number(self, number):
        return Matrix(*[list(x + number) for x in self.elements])

    def __sub_matrix(self, matrix):
        return Matrix(*[list(x - y) for x, y in
                      zip(self.elements, matrix.elements)])

    def __sub_number(self, number):
        return Matrix(*[list(x - number) for x in self.elements])

    def __add__(self, other):
        """
        Depending on the argument either adds a number to the elements
        of the matrix or adds two matrices. Returns a new object.

        Example(number):
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]) + 2
        => Matrix([3, 4, 5], [6, 7, 8], [9, 10, 11])

        Example(matrix):
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]) +
           Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9])
        => Matrix([2, 4, 6], [8, 10, 12], [14, 16, 18])

        Arguments:
        number : (Numeric)
        or
        matrix : (Matrix)
        """
        if type(other) is Matrix:
            if self.is_same_dimension(other):
                return self.__add_matrix(other)
            else:
                raise MatrixDimensionError(
                    "Can't add matrices with different dimensions")
        else:
            return self.__add_number(other)

    def __iadd__(self, other):
        """
        Depending on the argument either adds a number to the elements
        of the matrix or adds two matrices. Changes the object.

        Example(number):
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]) + 2
        => Matrix([3, 4, 5], [6, 7, 8], [9, 10, 11])

        Example(matrix):
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]) +
           Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9])
        => Matrix([2, 4, 6], [8, 10, 12], [14, 16, 18])

        Arguments:
        number : (Numeric)
        or
        matrix : (Matrix)
        """
        self = self + other
        return self

    def __sub__(self, other):
        """
        Depending on the argument either substracts a number from the
        elements of the matrix or substracts two matrices.
        Returns a new object.

        Example(number):
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]) - 2
        => Matrix([-1, 0, 1], [2, 3, 4], [5, 6, 7])

        Example(matrix):
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]) -
           Matrix([1, 1, 3], [2, 5, 7], [2, 3, 4])
        => Matrix([0, 1, 0], [2, 0, -1], [5, 5, 5])

        Arguments:
        number : (Numeric)
        or
        matrix : (Matrix)
        """
        if type(other) is Matrix:
            if self.is_same_dimension(other):
                return self.__sub_matrix(other)
            else:
                raise MatrixDimensionError(
                    "Can't substitute matrices with different dimensions")
        else:
            return self.__sub_number(other)

    def __isub__(self, other):
        """
        Depending on the argument either substracts a number from the elements
        of the matrix or substracts two matrices. Changes the object.

        Example(number):
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]) - 2
        => Matrix([-1, 0, 1], [2, 3, 4], [5, 6, 7])

        Example(matrix):
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]) -
           Matrix([1, 1, 3], [2, 5, 7], [2, 3, 4])
        => Matrix([0, 1, 0], [2, 0, -1], [5, 5, 5])

        Arguments:
        number : (Numeric)
        or
        matrix : (Matrix)
        """
        self = self - other
        return self

    def __getitem__(self, index):
        """
        Access the elements of the matrix with the [] operator.

        Example:
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9])[1]
        => Vector(4, 5, 6)

        Example:
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9])[1][2]
        => 6

        Arguments:
        number : (int)
        """
        return self.elements[index]

    def transposed(self):
        """
        Tranposes a matrix. Returns a new object.

        Example:
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]).transposed()
        => Matrix([1, 4, 7], [2, 5, 8], [3, 6, 9])

        Arguments:
        No arguments
        """
        return Matrix(*[self.get_colum(i) for i in range(self.colums())])

    def transpose(self):
        """
        Tranposes a matrix. Changes the object.

        Example:
        >> Matrix([1, 2, 3], [4, 5, 6], [7, 8, 9]).transpose()
        => Matrix([1, 4, 7], [2, 5, 8], [3, 6, 9])
        Arguments:
        No arguments
        """
        self.elements = self.transposed().elements
        return self

    def __mul__(self, other):
        """