'''
Created on 07-Feb-2021

@author: bthakur
'''
import matplotlib.pyplot as plt

from functools import reduce
from ...util import MathUtil
from ._matrix_solver import MatrixSolver

import numpy as np
import random
import inspect
import traceback

from xv.km.ui import SvgHelper


from sympy import (simplify, sympify, symbols,
                   Rational, latex, solve,
                   Matrix, Eq, linear_eq_to_matrix,
                   lambdify, MatAdd, matrix2numpy,
                   )

from xv.km.util import latex_fraction

from ...helper import BaseHelper
from xv.km.managers import BaseManager
from xv.km.kelements import KProblemTemplate

class LinearEquationManager(BaseManager):
    '''
    Solving linear equations with help algebra and geometry


    Minimum Grade: 4
    Maximum Grade: 12


    Examples
    --------
    ke = LinearEquationManager()
    ke

    ke.printProblemTypes()
    ke.helpProblemType(0)

    ke.getRandomProblem()
    ke.getRandomProblem(problem_type = 0)
    ke.getRandomProblem(problem_type = 0, dim = 2)


    doc_style: xv_doc
    '''



    def __new__(cls, *args, **kwargs):
        instance = super(LinearEquationManager, cls).__new__(cls, *args, **kwargs)
        for base_class in LinearEquationManager.__bases__:
            base_class.__init__(instance, *args, **kwargs)
        return instance

    def __init__(self, *args, **kwargs):
        self.base_helper = BaseHelper(**kwargs)
        self.A = None
        self.X = None
        self.B = None
        pass


    def generateNotebook(self,
                         templateName = None,
                         saveToFile = None,
                         include_path = None,
                         saveToFolder = None,
                          *args,
                          **kwargs):

        if kwargs.get('size', None) is None:
            nSize = random.randint(2, 5)
            kwargs['size'] = (nSize, nSize)
            pass

        return super().generateNotebook(templateName = templateName,
                                        saveToFile = saveToFile,
                                        include_path = include_path,
                                        saveToFolder = saveToFolder,
                                        *args,
                                        **kwargs
                                        )

    def _getRandomMatrix(self, size):
        signs = np.random.choice([-1,1], size)
        arr = np.random.randint(1, 10, size) * signs
        A = Matrix(arr)
        return A
        pass

    def _getRandomVars(self, dim):
        var_names = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.lower())
        return Matrix(var_names[-1 *dim: ])
        pass

    def getEquations(self):
        if self._problem is None:
            self.getRandomProblem()
            pass
        return self._getEquations(self.A, self.X, self.B)
        pass



    def init_problem_data(self, *args, **kwargs):
        '''
        Example:
            ke = LinearEquationManager()
            ke.init_problem_data()
            ke.init_problem_data(dim = 2)
            ke.init_problem_data(dim = 3)
        '''
        dim = kwargs.get("dim", 3)
        AB = self._getRandomMatrix( (dim, dim + 1) )
        A = AB[:, :-1]
        X = self._getRandomVars(dim)
        B = self._getRandomMatrix( (1, dim) )
        self.A = A
        self.X = X
        self.B = B
        self.eqs = self._getEquations(self.A, self.X, self.B)
        return A, X, B, self.eqs
        pass

    def _getEquations(self, A, X, B):
        eqs = []
        for i in range(X.rows):
            expr = (A[i, :] * X)[0]
            eq = Eq(expr , B[i])
            eqs.append(eq)
            pass
        return eqs
        pass

    def _initProblem(self, A, X, B):
        print("TODO")
        return None
        pass

    def plot(self,
             plotType = 'Row',
             xlim = None,
             ylim = None,
             zlim = None,
             color = ['red', 'blue', 'green', 'navy', 'orange', 'purple'],
             **kwargs):

        if len(self.eqs) < 2 or len(self.eqs) > 3:
            print("Only 2 or 3 dimensions are supported")
            return None

        if plotType.lower() == 'Row'.lower():
            if len(self.eqs) == 2:
                self._plot2DEq(xlim, ylim, color, **kwargs)
            elif len(self.eqs) == 3:
                self._plot3DEq(xlim, ylim, zlim, color, **kwargs)

        elif plotType.lower() == 'Basis'.lower():
            if len(self.eqs) == 2:
                self._plot2DBasis(xlim, ylim, color, **kwargs)
            elif len(self.eqs) == 3:
                self._plot3DBasis(xlim, ylim, zlim, color, **kwargs)

        elif plotType.lower() == 'Column'.lower() or plotType.lower() == 'Coln'.lower():
            if len(self.eqs) == 2:
                self._plot2DColn(xlim, ylim, color, **kwargs)
            elif len(self.eqs) == 3:
                self._plot3DColn(xlim, ylim, zlim, color, **kwargs)

    def _plot3DBasis(self, xlim, ylim, zlim, color = ['r', 'b', 'g'], **kwargs):
        ax = self._plot3dFigure(figsize = (8,8))
        A, B, X = self._eqToMatrix()
        listOfColns = [self._getColn(A, i) for i in range(len(self.X))]
        origin = [0, 0, 0]

        if xlim == None and ylim == None:
            xlim = [- float(max(list(A))) - 1,
                    float(max(list(A))) + 1]
            ylim = [- float(max(list(A))) - 1,
                    float(max(list(A))) + 1]
            zlim = [- float(max(list(A))) - 1,
                    float(max(list(A))) + 1]

        for i in range(len(listOfColns)):
            ax.quiver(origin[0],
                      origin[1],
                      origin[2],
                      float(listOfColns[i][0]),
                      float(listOfColns[i][1]),
                      float(listOfColns[i][2]),
                      color = color[i],
                      label = f'{list(listOfColns[i])}, basis vector{i + 1}',
                      **kwargs)
        plt.legend()
        ax.set_xlim(*xlim)
        ax.set_ylim(*ylim)
        ax.set_zlim(*zlim)
        #THAKUR #plt.show()

    def _plot3dFigure(self, **kwargs):
        fig = plt.figure(**kwargs)
        ax = plt.axes(projection='3d', box_aspect = (1, 1, 1))
        return ax

    def _eqToMatrix(self):
        A, B = linear_eq_to_matrix(self.eqs, *self.X)
        X = Matrix(self.X)
        return A, B, X

    def _getColn(self, matrix, colnIndex):
        return matrix.col(colnIndex)

    def _plot3DEq(self, xlim, ylim, zlim, color = ['r', 'b', 'g'], **kwargs):
        ax = self._plot3dFigure(figsize = (8,8))
        if xlim is None: xlim = [-30, 30]
        xData = np.outer(np.linspace(xlim[0], xlim[1], 50), np.ones(50))
        yData = xData.copy().T
        zData = list()
        for eq in self.eqs:
            output = self._3dCoordFunc(xData, yData, eq, self.X[2])
            zData.append(output)
        plots = list()
        for i in range(len(zData)):
            p = ax.plot_surface(xData,
                                yData,
                                zData[i],
                                color = color[i],
                                label = f'${latex(self.eqs[i])}$',
                                alpha = 0.5,
                                **kwargs)
            plots.append(p)
        plots[0]._facecolors2d = plots[0]._facecolor3d
        plots[0]._edgecolors2d = plots[0]._edgecolor3d

        plots[1]._facecolors2d = plots[1]._facecolor3d
        plots[1]._edgecolors2d = plots[1]._edgecolor3d

        plots[2]._facecolors2d = plots[2]._facecolor3d
        plots[2]._edgecolors2d = plots[2]._edgecolor3d

        ax.plot(self._solve()[self.X[0]],
                self._solve()[self.X[1]],
                self._solve()[self.X[2]],
                marker = 'o',
                color = 'm',
                markersize = 10,
                label = str(self._solveList()))

        ax.text(self._solve()[self.X[0]],
                self._solve()[self.X[1]],
                self._solve()[self.X[2]],
                str(self._solveList()),
                fontsize=12)


        plt.legend()
        if xlim != None and ylim != None and zlim != None:
            ax.set_xlim(*xlim)
            ax.set_ylim(*ylim)
            ax.set_zlim(*zlim)
        #THAKUR #plt.show()

    def _3dCoordFunc(self, coords1, coords2, eq, var):
        val = np.ones(shape = coords1.shape)
        soln = solve(eq, var)[0]
        vars = []
        for idx in range(self.X.rows):
            vars.append(self.X[idx])
        vars.remove(var)
        func = lambdify(vars, soln)
        for i in range(coords1.shape[0]):
            for j in range(coords1.shape[1]):
                val[i, j] = float(func(coords1[i, j], coords2[i, j]))

        return val

    def _solve(self):
        return solve(self.eqs, *self.X)

    def _solveList(self):
        solnList = list(self._solve().values())
        solnList = [i.n(2) for i in solnList]
        return solnList

    def _plot3DColn(self, xlim, ylim, zlim, color = ['red', 'blue', 'green', 'navy', 'orange', 'purple'], **kwargs):
        ax = self._plot3dFigure(figsize = (8,8))
        A, B, X = self._eqToMatrix()
        listOfColns = [self._getColn(A, i).n(2) for i in range(len(self.X))]
        SolnListOfColns = [(self._getColn(A, i) * self._solve()[self.X[i]]).n(2) for i in range(len(self.X))]
        origin = [0, 0, 0]

        if xlim == None and ylim == None and zlim == None:
            dummyList = [matrix2numpy(A, dtype = float).flatten(),
                         matrix2numpy(B, dtype = float).flatten(),
                         matrix2numpy(Matrix(SolnListOfColns)).flatten()]
            dummyList = [abs(float(j)) for i in dummyList for j in i]
            xlim = [- float(max(dummyList)) - 1,
                    float(max(dummyList)) + 1]
            ylim = [- float(max(dummyList)) - 1,
                    float(max(dummyList)) + 1]
            zlim = [- float(max(dummyList)) - 1,
                    float(max(dummyList)) + 1]

        for i in range(len(listOfColns)):
            ax.quiver(origin[0],
                      origin[1],
                      origin[2],
                      float(listOfColns[i][0]),
                      float(listOfColns[i][1]),
                      float(listOfColns[i][2]),
                      color = color[i],
                      label = f'{list(listOfColns[i])}, basis vector{i + 1}',
                      alpha = 0.3,
                      **kwargs)

        for i in range(len(SolnListOfColns)):
            if i == 0:
                ax.quiver(origin[0],
                          origin[1],
                          origin[2],
                          float(SolnListOfColns[i][0]),
                          float(SolnListOfColns[i][1]),
                          float(SolnListOfColns[i][2]),
                          color = color[i + 3],
                          label = f'{list(SolnListOfColns[i])}, coln vector{i + 1}',
                          **kwargs)
            else:
                ax.quiver(float(MatAdd(*SolnListOfColns[:i], evaluate = True)[0]),
                          float(MatAdd(*SolnListOfColns[:i], evaluate = True)[1]),
                          float(MatAdd(*SolnListOfColns[:i], evaluate = True)[2]),
                          float(SolnListOfColns[i][0]),
                          float(SolnListOfColns[i][1]),
                          float(SolnListOfColns[i][2]),
                          color = color[i + 3],
                          label = f'{list(SolnListOfColns[i])}, coln vector{i + 1}',
                          **kwargs)

        ax.quiver(origin[0],
                  origin[1],
                  origin[2],
                  float(B[0]),
                  float(B[1]),
                  float(B[2]),
                  color = 'black',
                  label = 'result vector',
                  **kwargs)

        ax.plot(float(B[0]),
                float(B[1]),
                float(B[2]),
                marker = 'o',
                color = 'r',
                markersize = 8,
                label = f'{list(B)}, result')

        ax.text(float(B[0]),
                float(B[1]),
                float(B[2]),
                f'${list(B)}$',
                fontsize=12)
        plt.legend()
        ax.set_xlim(*xlim)
        ax.set_ylim(*ylim)
        ax.set_zlim(*zlim)
        #THAKUR #plt.show()

    def _plot2DBasis(self, xlim, ylim, color = ['r', 'b'], **kwargs):
        fig = self._plot2dFigure(figsize = (8, 8))
        A, B, X = self._eqToMatrix()
        listOfColns = [self._getColn(A, i) for i in range(len(self.X))]
        origin = [0, 0]

        if xlim == None and ylim == None:
            dummyList = [matrix2numpy(A, dtype = float).flatten(),
                         matrix2numpy(B, dtype = float).flatten()]
            dummyList = [abs(float(j)) for i in dummyList for j in i]
            xlim = [- float(max(dummyList)) - 1,
                    float(max(dummyList)) + 1]
            ylim = [- float(max(dummyList)) - 1,
                    float(max(dummyList)) + 1]

        for i in range(len(listOfColns)):
            plt.arrow(origin[0],
                      origin[1],
                      float(listOfColns[i][0]),
                      float(listOfColns[i][1]),
                      width = 0.05,
                      color = color[i],
                      label = f'{list(listOfColns[i])}, basis vector{i + 1}',
                      length_includes_head = True,
                      **kwargs)
        plt.xlim(*xlim)
        plt.ylim(*ylim)
        plt.gca().set_aspect('equal','box')
        plt.grid()
        plt.legend()
        #THAKUR #plt.show()

    def _plot2dFigure(self, **kwargs):
        return plt.figure(**kwargs)

    def _plot2DEq(self, xlim, ylim, color = ['b', 'g'], **kwargs):
        fig = self._plot2dFigure(figsize = (8, 8))
        if xlim == None and ylim == None:
            xlim = self._dynamicLimit()
            ylim = self._dynamicLimit()
        xData = np.arange(*xlim, 0.01)
        yData = list()
        for eq in self.eqs:
            output = self._2dCoordFunc(xData, eq, self.X[1])
            yData.append(output)

        for i in range(len(yData)):
            plt.plot(xData,
                     yData[i],
                     color = color[i],
                     label = f'${latex(self.eqs[i])}$',
                     **kwargs)
        plt.plot(self._solve()[self.X[0]],
                 self._solve()[self.X[1]],
                 marker = 'o',
                 color = 'g')
        plt.annotate(f'{self._solveList()}',
                     xy = (self._solve()[self.X[0]] + 1,
                           self._solve()[self.X[1]]),
                     fontsize=15)


        plt.xlim(*xlim)
        plt.ylim(*ylim)
        plt.xlabel(f'${self.X[0]}$', fontsize=15)
        plt.ylabel(f'${self.X[1]}$', fontsize=15)
        plt.gca().set_aspect('equal','box')
        plt.grid()
        plt.legend()
        #THAKUR #plt.show()

    def _dynamicLimit(self):
        lmt = [-max([abs(float(i)) for i in self._solveList()]) - 10,
               max([abs(float(i)) for i in self._solveList()]) + 10]
        return lmt

    def _2dCoordFunc(self, inputs, eq, var):
        val = list()
        soln = solve(eq, var)[0]
        subSym = list(soln.free_symbols)[0]
        for i in inputs:
            output = float(soln.subs(subSym, i).n())
            val.append(output)
        return np.array(val)

    def _plot2DColn(self, xlim, ylim, color = ['r', 'b', 'g', 'm'], **kwargs):
        fig = self._plot2dFigure(figsize = (8, 8))
        A, B, X = self._eqToMatrix()
        listOfColns = [self._getColn(A, i) for i in range(len(self.X))]
        SolnListOfColns = [list(self._getColn(A, i) * self._solve()[self.X[i]]) for i in range(len(self.X))]

        origin = [0, 0]

        if xlim == None and ylim == None:
            dummyList = [matrix2numpy(A, dtype = float).flatten(),
                         matrix2numpy(B, dtype = float).flatten(),
                         matrix2numpy(Matrix(SolnListOfColns)).flatten()]
            dummyList = [abs(float(j)) for i in dummyList for j in i]
            xlim = [- float(max(dummyList)) - 1,
                    float(max(dummyList)) + 1]
            ylim = [- float(max(dummyList)) - 1,
                    float(max(dummyList)) + 1]

        for i in range(len(listOfColns)):
            plt.arrow(origin[0],
                      origin[1],
                      float(listOfColns[i][0]),
                      float(listOfColns[i][1]),
                      width = 0.05,
                      color = color[i],
                      label = f'{list(listOfColns[i])}, basis vector{i + 1}',
                      length_includes_head = True,
                      alpha = 0.5,
                      **kwargs)

        for i in range(len(SolnListOfColns)):
            if i == 0:
                plt.arrow(origin[0],
                          origin[1],
                          float(SolnListOfColns[i][0]),
                          float(SolnListOfColns[i][1]),
                          width = 0.05,
                          color = color[i + 2],
                          label = f'{list(SolnListOfColns[i])}, coln vector{i + 1}',
                          length_includes_head = True,
                          **kwargs)
            else:
                plt.arrow(float(SolnListOfColns[i - 1][0]),
                          float(SolnListOfColns[i - 1][1]),
                          float(SolnListOfColns[i][0]),
                          float(SolnListOfColns[i][1]),
                          width = 0.05,
                          color = color[i + 2],
                          label = f'{list(SolnListOfColns[i])}, coln vector{i + 1}',
                          length_includes_head = True,
                          **kwargs)

        plt.arrow(origin[0],
                  origin[1],
                  float(B[0]),
                  float(B[1]),
                  width = 0.05,
                  color = 'black',
                  label = 'result vector',
                  length_includes_head = True,
                  **kwargs)

        plt.plot(float(B[0]),
                 float(B[1]),
                 marker = 'o',
                 color = 'r',
                 markersize = 8,
                 label = f'{list(B)}, result')

        plt.annotate(f'${list(B)}$',
                     xy = (B[0] + 1,
                           B[1]),
                     fontsize=12)
        plt.xlim(*xlim)
        plt.ylim(*ylim)
        plt.gca().set_aspect('equal','box')
        plt.grid()
        plt.legend()
        #THAKUR #plt.show()



    def _init_problem_types(self, *args, **kwargs):
        self._append_problem_types([
            '_problem_solve_le_by_row_operations',
            '_problem_solve_a_determinant',
            '_problem_solve_le_by_crammer_rule',
            '_problem_solve_le_by_matrix_row_operation',
            '_problem_solve_le_by_matrix_inverse',
            '_problem_solve_le_graphically',
            '_problem_plot_basis',
            '_problem_plot_columns',
            '_problem_fractional_equations',
            '_problem_fractional_decimal_equations',
            '_problem_jacobi_method',
         ])
        pass

    def _problem_solve_le_by_row_operations(self, *args, **kwargs):
        """
        Solve linear equations by row operations


        Minimum Grade: 4
        Maximum Grade: 12



        Examples
        --------
        from xv.math.algebra import LinearEquationManager
        ke = LinearEquationManager()


        ke.getRandomProblem(problem_type = '_problem_solve_le_by_row_operations')
        ke.getRandomProblem(problem_type = '_problem_solve_le_by_row_operations', dim = 2)


        doc_style: xv_doc
        """


        A, X, B, eqs = self.init_problem_data(*args, **kwargs)
        matrixSolver = MatrixSolver(A, X, B)
        self.eq_solutions = matrixSolver.getSolution()

        solution_type = 'row_operations'
        desired_solution = self.eq_solutions[solution_type]

        eqs_str = "\n".join([f"${latex(eq)}$" for eq in eqs])

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Solve the linear equation by {solution_type}

        {eqs_str}
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')

        for item_idx, item in enumerate(desired_solution):
            if item_idx < 2:
                continue
            problemTemplate.add_solution(item, 'text/html')


        soln_s = f"""
        Note: You may change dimension of equations by passing dim. For example:
        dim = 2
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
         $\\displaystyle {latex(solve(eqs))}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')

        return problemTemplate

    def _problem_solve_a_determinant(self, *args, **kwargs):
        """
        how to calculate value of a determinant


        doc_style: xv_doc
        """

        matrix_helper = self.base_helper.get_algebra_helper().get_matrix_helper()
        result = matrix_helper.get_problem_solve_a_determinant()

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        if result is None:
            prob_s = f"""
            Failed to get questions
            """
            problemTemplate.add_problem(prob_s.strip(), 'text/html')
            return problemTemplate

        prob_s = f"""
        {result['prob_s']}
        """
        problemTemplate.add_problem(prob_s.strip(), 'text/html')

        soln_s = f"""
        {result['soln_s']}
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        {result['answer_s']}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')

        return problemTemplate
        pass


    def _problem_solve_le_by_crammer_rule(self, *args, **kwargs):
        """
        Solve linear equations by crammer rule


        Minimum Grade: 4
        Maximum Grade: 12



        Examples
        --------
        from xv.math.algebra import LinearEquationManager
        ke = LinearEquationManager()


        ke.getRandomProblem(problem_type = '_problem_solve_le_by_crammer_rule')
        ke.getRandomProblem(problem_type = '_problem_solve_le_by_crammer_rule', dim = 2)


        doc_style: xv_doc
        """


        A, X, B, eqs = self.init_problem_data(*args, **kwargs)
        matrixSolver = MatrixSolver(A, X, B)
        self.eq_solutions = matrixSolver.getSolution()

        solution_type = 'crammer_rule'
        desired_solution = self.eq_solutions[solution_type]

        eqs_str = "\n".join([f"${latex(eq)}$" for eq in eqs])

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Solve the linear equation by {solution_type}

        {eqs_str}

        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')

        for item_idx, item in enumerate(desired_solution):
            if item_idx < 2:
                continue
            problemTemplate.add_solution(item, 'text/html')

        soln_s = f"""
        Note: You may change dimension of equations by passing dim. For example:
        dim = 2
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
         $\\displaystyle {latex(solve(eqs))}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')

        return problemTemplate



    def _problem_solve_le_by_matrix_row_operation(self, *args, **kwargs):
        """
        Solve linear equations by matrix row operations


        Minimum Grade: 4
        Maximum Grade: 12



        Examples
        --------
        from xv.math.algebra import LinearEquationManager
        ke = LinearEquationManager()


        ke.getRandomProblem(problem_type = '_problem_solve_le_by_matrix_row_operation')
        ke.getRandomProblem(problem_type = '_problem_solve_le_by_matrix_row_operation', dim = 2)


        doc_style: xv_doc
        """


        A, X, B, eqs = self.init_problem_data(*args, **kwargs)
        matrixSolver = MatrixSolver(A, X, B)
        self.eq_solutions = matrixSolver.getSolution()

        solution_type = 'matrix_row_operation'
        desired_solution = self.eq_solutions[solution_type]

        eqs_str = "\n".join([f"${latex(eq)}$" for eq in eqs])

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Solve the linear equation by {solution_type}

        {eqs_str}

        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')

        for item_idx, item in enumerate(desired_solution):
            if item_idx < 2:
                continue
            problemTemplate.add_solution(item, 'text/html')

        soln_s = f"""
        Note: You may change dimension of equations by passing dim. For example:
        dim = 2
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
         $\\displaystyle {latex(solve(eqs))}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')

        return problemTemplate

    def _problem_solve_le_by_matrix_inverse(self, *args, **kwargs):
        """
        Solve linear equations by matrix inverse


        Minimum Grade: 4
        Maximum Grade: 12



        Examples
        --------
        from xv.math.algebra import LinearEquationManager
        ke = LinearEquationManager()


        ke.getRandomProblem(problem_type = '_problem_solve_le_by_matrix_inverse')
        ke.getRandomProblem(problem_type = '_problem_solve_le_by_matrix_inverse', dim = 2)


        doc_style: xv_doc
        """


        A, X, B, eqs = self.init_problem_data(*args, **kwargs)
        self.B = self.B.T
        B = self.B
        solution_type = 'matrix inverse'

        eqs_str = "\n".join([f"${latex(eq)}$" for eq in eqs])

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Solve the linear equation by using {solution_type}

        {eqs_str}

        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')

        soln_s = f"""
        {eqs_str}

        $A$ = ${latex(self.A)}$

        $X$ = ${latex(self.X)}$

        $B$ = ${latex(self.B)}$

        The equation is:
        ${latex(self.A)} {latex(self.X)}$ = ${latex(self.B)}$

        In short:
        $AX = B$
        $\\implies A^{{-1}}AX = A^{{-1}} B$
        $\\implies X = A^{{-1}} B$

        $A^{{-1}}$ = ${latex(self.A.inv())}$

        $X$ = $A^{{-1}}$ * $B$ = ${latex(self.A.inv() * self.B)}$

        """

        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        soln_s = f"""
        Note: You may change dimension of equations by passing dim. For example:
        dim = 2
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
         $\\displaystyle {latex(solve(eqs))}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')

        return problemTemplate

    def _problem_solve_le_graphically(self, *args, **kwargs):
        """
        Solve linear equations by graphically


        Minimum Grade: 4
        Maximum Grade: 12



        Examples
        --------
        from xv.math.algebra import LinearEquationManager
        ke = LinearEquationManager()


        ke.getRandomProblem(problem_type = '_problem_solve_le_graphically')
        ke.getRandomProblem(problem_type = '_problem_solve_le_graphically', dim = 2)


        doc_style: xv_doc
        """


        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        dim = kwargs.get("dim", 3)
        if not dim in [2, 3]:
            problem_s = f"""
            Invalid dim = {dim}
            dim must be 2 or 3
            """
            problemTemplate.add_problem(problem_s.strip(), 'text/html')
            return problemTemplate
            pass

        A, X, B, eqs = self.init_problem_data(*args, **kwargs)
        eqs_str = "\n".join([f"${latex(eq)}$" for eq in eqs])

        problem_s = f"""
        Plot equations to solve

        {eqs_str}
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')

        plt.ioff()
        plt.figure(figsize = (10, 8))
        self.plot()
        svgHelper = SvgHelper()
        svg_str = svgHelper.plt_to_svg()
        plt.clf()
        plt.close()

        soln_s = f"""
        ke.plot()
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        problemTemplate.add_solution(svg_str, 'text/svg')

        soln_s = f"""
        Note: You may change dimension of equations by passing dim. For example:
        dim = 2 or
        dim = 3
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
         $\\displaystyle {latex(solve(eqs))}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        problemTemplate.add_answer(svg_str, 'text/svg')

        return problemTemplate


    def _problem_plot_basis(self, *args, **kwargs):
        """
        Plot basis vectors


        Minimum Grade: 4
        Maximum Grade: 12



        Examples
        --------
        from xv.math.algebra import LinearEquationManager
        ke = LinearEquationManager()


        ke.getRandomProblem(problem_type = '_problem_plot_basis')
        ke.getRandomProblem(problem_type = '_problem_plot_basis', dim = 2)


        doc_style: xv_doc
        """


        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        dim = kwargs.get("dim", 3)
        if not dim in [2, 3]:
            problem_s = f"""
            Invalid dim = {dim}
            dim must be 2 or 3
            """
            problemTemplate.add_problem(problem_s.strip(), 'text/html')
            return problemTemplate
            pass

        A, X, B, eqs = self.init_problem_data(*args, **kwargs)
        eqs_str = "\n".join([f"${latex(eq)}$" for eq in eqs])

        problem_s = f"""
        Plot basis of equations

        {eqs_str}
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')

        plt.ioff()
        plt.figure(figsize = (10, 8))
        self.plot(plotType = 'Basis')
        svgHelper = SvgHelper()
        svg_str = svgHelper.plt_to_svg()
        #plt.figure().clear()
        plt.clf()
        plt.close()


        soln_s = f"""
        ke.plot(plotType = 'Basis')
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        problemTemplate.add_solution(svg_str, 'text/svg')

        answer_s = f"""
        ke.plot(plotType = 'Basis')
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        problemTemplate.add_answer(svg_str, 'text/svg')

        return problemTemplate

    def _problem_plot_columns(self, *args, **kwargs):
        """
        Plot column vectors


        Minimum Grade: 4
        Maximum Grade: 12



        Examples
        --------
        from xv.math.algebra import LinearEquationManager
        ke = LinearEquationManager()


        ke.getRandomProblem(problem_type = '_problem_plot_columns')
        ke.getRandomProblem(problem_type = '_problem_plot_columns', dim = 2)


        doc_style: xv_doc
        """


        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        dim = kwargs.get("dim", 3)
        if not dim in [2, 3]:
            problem_s = f"""
            Invalid dim = {dim}
            dim must be 2 or 3
            """
            problemTemplate.add_problem(problem_s.strip(), 'text/html')
            return problemTemplate
            pass

        A, X, B, eqs = self.init_problem_data(*args, **kwargs)
        eqs_str = "\n".join([f"${latex(eq)}$" for eq in eqs])

        problem_s = f"""
        Plot columns of the equations

        {eqs_str}
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')

        plt.ioff()
        plt.figure(figsize = (10, 8))
        self.plot(plotType = 'coln')
        svgHelper = SvgHelper()
        svg_str = svgHelper.plt_to_svg()
        #plt.figure().clear()
        plt.clf()
        plt.close()



        soln_s = f"""
        ke.plot(plotType = 'coln')
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        problemTemplate.add_solution(svg_str, 'text/svg')

        answer_s = f"""
        ke.plot(plotType = 'coln')
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        problemTemplate.add_answer(svg_str, 'text/svg')

        return problemTemplate


    def _problem_fractional_equations(self, *args, **kwargs):
        """
        _sample

        Minimum Grade: 4
        Maximum Grade: 12



        Examples
        --------
        from xv.math.algebra import LinearEquationManager
        ke = LinearEquationManager()


        ke.getRandomProblem(problem_type = '_problem_fractional_equations')


        doc_style: xv_doc
        """
        arr = [a1, a2, a3, b1, b2, b3] = [Rational(random.randint(1, 9),
                                           random.randint(1, 9))
                                  for i in range(6)]

        ad1, ad2, ad3, bd1, bd2, bd3 = [num.q for num in arr]

        x, y = symbols("x, y")
        eqa = Eq(a1 * x + a2 * y, a3)
        eqb = Eq(b1 * x + b2 * y, b3)

        fctr_a = MathUtil.lcm(ad1, ad2, ad3)
        fctr_b = MathUtil.lcm(bd1, bd2, bd3)

        eqa_1 = Eq(fctr_a * a1 * x + fctr_a * a2 * y, fctr_a * a3)
        eqb_1 = Eq(fctr_b * b1 * x + fctr_b * b2 * y, fctr_b * b3)

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Simplify equations into simpler forms

        ${latex(eqa)}$

        ${latex(eqb)}$

        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')

        soln_s = f"""
        <h4>First Equation</h4>
        ${latex(eqa)}$

        Multiply it with denominators of each term:

        $\\implies \\left( {latex(eqa)} \\right)
        * {fctr_a} $ As lcm({ad1}, {ad2}, {ad3}) = {fctr_a}

        $\\implies {latex(eqa_1)}$

        <h4>Second Equation</h4>
        ${latex(eqb)}$

        Multiply it with denominators of each term:

        $\\implies \\left( {latex(eqb)} \\right)
        * {fctr_b}$ As lcm({bd1}, {bd2}, {bd3}) = {fctr_b}

        $\\implies {latex(eqb_1)}$

        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        ${latex(eqa_1)}$

        ${latex(eqb_1)}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')

        return problemTemplate




    def _problem_fractional_decimal_equations(self, *args, **kwargs):
        """
        _problem_fractional_equations

        Minimum Grade: 4
        Maximum Grade: 12



        Examples
        --------
        from xv.math.algebra import LinearEquationManager
        ke = LinearEquationManager()


        ke.getRandomProblem(problem_type = '_problem_fractional_decimal_equations')


        doc_style: xv_doc
        """
        ns = [an1, an2, an3, bn1, bn2, bn3] = [random.randint(1, 9)
                                          for i in range(6)]

        ds = [ad1, ad2, ad3, bd1, bd2, bd3] = [random.randint(1, 9)
                                          for i in range(6)]

        for i in range(len(ns)):
            if ns[i] == ds[i]:
                ns[i] = 1
                ds[i] = 1
            pass


        n_arr = []
        denr_arr = []
        from xv.util import toIntOrFloat
        for i in range(len(ns)):
            if ns[i] / ds[i] == round(ns[i] / ds[i], 2):
                n_arr.append(toIntOrFloat(ns[i] / ds[i]))
                denr_arr.append(1)
            else:
                n_arr.append(Rational(ns[i], ds[i]))
                denr_arr.append(ds[i])
            pass

        a1, a2, a3, b1, b2, b3 = n_arr
        add1, add2, add3, bdd1, bdd2, bdd3 = denr_arr

        x, y = symbols("x, y")
        eqa = Eq(a1 * x + a2 * y, a3)
        eqb = Eq(b1 * x + b2 * y, b3)

        fctr_a_1 = MathUtil.lcm(add1, add2, add3)
        fctr_b_1 = MathUtil.lcm(bdd1, bdd2, bdd3)

        fctr_a_2 = MathUtil.lcm(ad1, ad2, ad3)
        fctr_b_2 = MathUtil.lcm(bd1, bd2, bd3)


        #eqa_1 = Eq(fctr_a * a1 * x + fctr_a * a2 * y, fctr_a * a3)
        #eqb_1 = Eq(fctr_b * b1 * x + fctr_b * b2 * y, fctr_b * b3)

        eqa_1 = Eq(fctr_a_1 * a1 * x + fctr_a_1 * a2 * y, fctr_a_1 * a3)
        eqb_1 = Eq(fctr_b_1 * b1 * x + fctr_b_1 * b2 * y, fctr_b_1 * b3)

        eqa_2 = Eq(fctr_a_2 * a1 * x + fctr_a_2 * a2 * y, fctr_a_2 * a3)
        eqb_2 = Eq(fctr_b_2 * b1 * x + fctr_b_2 * b2 * y, fctr_b_2 * b3)

        #eqa_1 = (eqa * add1 * add2 * add3).cancel()
        #eqb_1 = (eqb * bdd1 * bdd2 * bdd3).cancel()

        #eqa_2 = (eqa * ad1 * ad2 * ad3).cancel()
        #eqb_2 = (eqb * bd1 * bd2 * bd3).cancel()

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Simplify equations into simpler forms

        ${latex(eqa)}$

        ${latex(eqb)}$

        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')

        soln_s = f"""
        <h2>Method 1</h2>
        <h4>First Equation</h4>
        ${latex(eqa)}$

        Make sure that each coefficient becomes an integer:

        $\\implies \\left( {latex(eqa)} \\right)
        * {fctr_a_1}
        $  As lcm({add1}, {add2}, {add3}) = {fctr_a_1}

        $\\implies {latex(eqa_1)}$

        <h4>Second Equation</h4>
        ${latex(eqb)}$

        Make sure that each coefficient becomes an integer:

        $\\implies \\left( {latex(eqb)} \\right)
        * {fctr_b_1} $ As lcm({bdd1}, {bdd2}, {bdd3}) = {fctr_b_1}

        $\\implies {latex(eqb_1)}$


        <h2>Method 2</h2>
        <h4>First Equation</h4>
        ${latex(eqa)}$

        Make sure that each coefficient becomes an integer:

        $\\implies \\left( {latex(eqa)} \\right)
        * {fctr_a_2} $ As lcm({ad1}, {ad2}, {ad3}) = {fctr_a_2}

        $\\implies {latex(eqa_2)}$

        <h4>Second Equation</h4>
        ${latex(eqb)}$

        Make sure that each coefficient becomes an integer:

        $\\implies \\left( {latex(eqb)} \\right)
        * {fctr_b_2}$ As lcm({bd1}, {bd2}, {bd3}) = {fctr_b_2}

        $\\implies {latex(eqb_2)}$

        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        <h2>Method 1</h2>
        ${latex(eqa_1)}$

        ${latex(eqb_1)}$

        <h2>Method 2</h2>
        ${latex(eqa_2)}$

        ${latex(eqb_2)}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')

        return problemTemplate

    def _problem_jacobi_method(self, *args, **kwargs):
        """
        _sample

        Minimum Grade: 4
        Maximum Grade: 12



        Examples
        --------
        from xv.math.algebra import LinearEquationManager
        ke = LinearEquationManager()


        ke.getRandomProblem(problem_type = '_sample')


        doc_style: xv_doc
        """


        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')



        import numpy as np
        from sympy import Symbol

        # initialize the matrix
        A = np.array([[10., -1., 2., 0.],
                      [-1., 11., -1., 3.],
                      [2., -1., 10., -1.],
                      [0.0, 3., -1., 8.]])

        b = np.array([6., 25., -11., 15.])
        MA, MX, MB, eqs = self.init_problem_data(*args, **kwargs)

        A = np.array(MA).astype(np.float64)
        b = np.array(MB).astype(np.float64)
        b = np.squeeze(b)
        MB = Matrix(b)

        MA_name = Symbol('A')
        MX_name = Symbol('X')
        MB_name = Symbol('B')


        ITERATION_LIMIT = 1000


        stmts = []
        x = np.zeros_like(b)
        for it_count in range(ITERATION_LIMIT):

            it_stmt = []

            Xe = Matrix(x)
            Xe_name = Symbol('X_e')

            it_stmt.append(f"""
            <h3>Iteration {it_count}:</h3>
            Estimated values $\\displaystyle
            {latex(Xe_name)} = {latex(Xe)}$
            """.strip())

            x_new = np.zeros_like(x)

            for i in range(A.shape[0]):
                s1 = np.dot(A[i, :i], x[:i])
                s2 = np.dot(A[i, i + 1:], x[i + 1:])
                x_new[i] = (b[i] - s1 - s2) / A[i, i]

                frac1 = latex_fraction(f"b[{i}] - s1 - s2",
                                       f"A[{i}, {i}]")

                frac2 = latex_fraction(f"{latex(b[i])} - {latex(s1)} - {latex(s2)}",
                                       f"{latex(A[i, i])}")

                frac3 = latex_fraction(f"{latex(b[i] - s1 - s2)}",
                                       f"{latex(A[i, i])}")

                it_stmt.append(f"""
                $\\displaystyle s1
                = np.dot({latex(MA_name)}[{i}, :{i}], {latex(Xe_name)}[:{i}])
                = {latex(Matrix(MA[i, :i]))} \\cdot {latex(Matrix(x[:i]))}
                = {latex(s1)}
                $

                $\\displaystyle s2
                = np.dot({latex(MA_name)}[{i}, {i + 1}:], {latex(Xe_name)}[{i + 1}:])
                = {latex(Matrix(MA[i, i + 1:]))} \\cdot {latex(Matrix(x[i + 1:]))}
                = {latex(s2)}
                $

                $\\displaystyle
                New Xe[{i}]
                = {frac1}
                = {frac2}
                = {frac3}
                = {latex(x_new[i])}
                $

                """.strip())

                if np.isnan(x_new[i]):
                    break

                if x_new[i] == x_new[i-1]:
                    break

                pass


            it_stmt = "\n\n".join(it_stmt)
            stmts.append(it_stmt)

            if np.allclose(x, x_new, atol=1e-10, rtol=0.):
                break

            for x_new_i in x_new:
                if np.isnan(x_new_i):
                    break

            x = x_new
            pass

        #reduce the records in stmts
        stmts = [stmts[0], stmts[1],
                 "...",
                 stmts[len(stmts)-2], stmts[len(stmts)-1]]

        Xe = Matrix(x)
        computed_val = np.dot(A, x)
        CV = Matrix(computed_val)
        CV_name = Symbol("C_v")

        error_val = computed_val - b
        EV = Matrix(error_val)
        EV_name = Symbol("E_v")


        eqs_str = []
        AX = MA * MX
        for i in range(AX.shape[0]):
            eqs_str.append(f"$\\displaystyle {latex(AX[i])} = {latex(MB[i])}$")
            pass

        eqs_str = "\n".join(eqs_str)
        stmts = "\n\n".join(stmts)

        problem_s = f"""
        Solve the following equations:

        {eqs_str}
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')

        soln_s = f"""
        Rewrite the equations as matrices:

        {eqs_str}

        $\\displaystyle {latex(MA)} {latex(MX)} = {latex(MB)}$

        In short:
        $\\displaystyle {latex(MA_name * MX_name)} = {latex(MB_name)}$


        <h2>Solve with Jacobi Method:</h2>
        {stmts}

        <h2>Solved Value with Jacobi Method:</h2>
        $\\displaystyle {latex(Xe_name)} = {latex(Xe)}
        $


        $\\displaystyle
        \\text{{Computed Value}}
        = {latex(CV_name)}
        = np.dot\\left({latex(MA_name)}, {latex(Xe_name)}\\right)
        $


        $\\displaystyle
        = {latex(MA)} * {latex(Xe)}
        = {latex(CV)}
        $


        $\\displaystyle
        \\text{{Error}} = {latex(EV_name)}
        = {latex(CV_name)} - {latex(MB_name)}
        = {latex(CV)} - {latex(MB)}
        = {latex(EV)}
        $

        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')

        return problemTemplate


    def _sample(self, *args, **kwargs):
        """
        _sample

        Minimum Grade: 4
        Maximum Grade: 12



        Examples
        --------
        from xv.math.algebra import LinearEquationManager
        ke = LinearEquationManager()


        ke.getRandomProblem(problem_type = '_sample')


        doc_style: xv_doc
        """


        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')

        soln_s = f"""
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')

        return problemTemplate

