'''
Created on 20-Jan-2022

@author: bthakur
'''

import codecs
import inspect
import traceback
import random
import numpy as np
import pandas as pd
from sympy import (latex, Symbol, symbols,
                   Matrix, Identity, nsimplify,
                    Rational,
                    sin, cos, tan,
                    asin, acos, atan,
                    sinh, cosh, tanh,
                    log, exp, sqrt,
                   diff, factor, 
                   Function, Eq,
                   Add, 
                   simplify, solve, factorial,
                   UnevaluatedExpr as ueval
                   )
import sympy as sp

from sympy.core.numbers import One, Integer
from sympy.series import series

from xv.km.util import (latex_fraction, strike_matrix_to_determinant, latex_color)


from xv.km.ui import SvgHelper
from xv.util import UtilClass

import matplotlib.pyplot as plt
from xv.util import CategorizedSampleData, list_to_str


class MatrixHelper(UtilClass):
    
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        pass
    
    
    

    def get_foundation_to_concept_of_matrix(self, 
                                            shop_count = None,
                                            interpretations = False,
                                            equations = False,
                                            solutions = False,
                                            inverse = False,
                                            row_operation = False,
                                            plot_equations = False,
                                            ):
        line_break = "##-##"

        #if plot_equations:
        #    human_count = 2
        #else:
        #    human_count = random.randint(2, 4)
            
        human_count = random.randint(2, 4)
        
        friend_names = CategorizedSampleData.get_human_names()
        friend_names = random.sample(friend_names, human_count)
        
        if plot_equations:
            food_count = 2
        elif solutions or inverse:
            food_count = human_count
        else:
            food_count = random.randint(2, 4)
            
        food_names = CategorizedSampleData.get_food_names()
        food_names = random.sample(food_names, food_count)
        
        if equations or plot_equations:
            shop_count = 1
        else:
            if shop_count is None:
                shop_count = random.randint(1, 4)
    
        if shop_count == 1:
            shop_names = ["Shop"]
            price_cols = ["price"]
            cost_cols = ["cost"]
        else:
            shop_names = [f"Shop{i+1}" for i in range(shop_count)]
            price_cols = [f"{x}{line_break}price" for x in shop_names]
            cost_cols = [f"{x}{line_break}cost" for x in shop_names]
            
        
        qty_arr = np.random.randint(0, high=5, size=(human_count, food_count), dtype=int)
        price_arr = np.random.randint(1, high=10, size=(food_count, shop_count), dtype=int)
        
        
        A = Matrix(qty_arr)
        Adf = pd.DataFrame(qty_arr, columns = food_names)
        Adf['friends'] = friend_names
        Adf = Adf.set_index('friends')
        
        B = Matrix(price_arr)
        Bdf = pd.DataFrame(price_arr, columns = price_cols)
        Bdf['items'] = food_names
        Bdf = Bdf.set_index('items')

        if A.shape[1] <= 3:
            var_symbs = symbols('x, y, z')
            var_symbs = var_symbs[:A.shape[1]]
        else:
            var_symbs = symbols('u, v, w, x, y, z')
            var_symbs = var_symbs[-A.shape[1]:]

        X = Matrix(var_symbs) 
        
        
        cost_arr = []
        cost_arr_latex = []
        for friend_idx in range(A.shape[0]):
            shop_cost = []
            shop_cost_latex = []
            for shop_idx in range(B.T.shape[0]):
                f_s_cost = 0
                f_s_cost_latex = []
                for item_ix in range(A.shape[1]):
                    q = A[friend_idx, item_ix]
                    p = B.T[shop_idx, item_ix]
                    f_s_cost = f_s_cost + q * p
                    f_s_cost_latex.append(f"{latex(q)} * {latex(p)}")
                    pass
                f_s_cost_latex = " + ".join(f_s_cost_latex)
                shop_cost.append(f_s_cost)
                shop_cost_latex.append(f_s_cost_latex)
                pass
            cost_arr.append(shop_cost)
            cost_arr_latex.append(shop_cost_latex)
            pass
        
        
        Cdf_latex = pd.DataFrame(cost_arr_latex, columns = cost_cols)
        Cdf_latex['friends'] = friend_names
        Cdf_latex = Cdf_latex.set_index('friends')

        C = Matrix(cost_arr)
        Cdf = pd.DataFrame(cost_arr, columns = cost_cols)
        Cdf['friends'] = friend_names
        Cdf = Cdf.set_index('friends')


        
        friend_names_str = list_to_str(friend_names, last_sep = 'and')
        food_names_str = list_to_str(food_names, last_sep = 'and')

        
        
        result = {
            'human_count': human_count,
            'friend_names': friend_names,
            'friend_names_str': friend_names_str,
            'food_count': food_count,
            'food_names': food_names,
            'food_names_str': food_names_str,
            'A': A,
            'B': B,
            'C': C,
            'X': X,
            'Adf': Adf,
            'Bdf': Bdf,
            'Cdf': Cdf,
            'Cdf_latex': Cdf_latex,
            'line_break': line_break,
            }
        
        if interpretations:
            qas_result = self._init_qas(A, B, C, friend_names, food_names, shop_names)
            result.update(qas_result)
       
        
        if equations or solutions:
            eqs_result = self._init_equations(A, B, C, X, friend_names, food_names, shop_names)
            result.update(eqs_result)
        
        if solutions:
            eqs_latex = result['eqs_latex']
            soln_s = self._init_solutions(A, B, C, X, eqs_latex)
            result['soln_s'] = soln_s
            pass
        
        if inverse:
            invert_matrix_result = self._get_invert_matrix(A, tol = None)
            if not invert_matrix_result is None:
                result.update(invert_matrix_result)
            pass
        
        if row_operation:
            row_operations_result = self._get_row_operations(A, X, C)
            if not row_operations_result is None:
                result.update(row_operations_result)
            pass
        
        if plot_equations:
            plot_equations_result = self._plot_equations(A, X, B, C)
            if not plot_equations_result is None:
                result.update(plot_equations_result)
            pass
        
        
        return result
        pass
    
    
    def _plot_equations(self, A, X, B, C):
        x_root = B[0, 0]
        
        from xv.plotter import Plotter2D
        
        plotter = Plotter2D()
        fig, ax = plotter.plot_matrix_equation(A, X, C, x_root = x_root)
                
        svgHelper = SvgHelper()
        svg_str = svgHelper.plt_to_svg()

        plt.clf()
        plt.close()
        
        result = {
            'svg_str': svg_str,
            }
        return result

    def _init_solutions(self, A, B, C, X, eqs_latex):
        
        soln_s = f"""
        {eqs_latex}
        <br/>
        <br/>
        $\\displaystyle \\implies 
        {latex(A)} {latex(X)} = {latex(C)}
        $
        <br/>
        <br/>
        $\\displaystyle \\implies 
        \\left( {latex(A)} \\right)^{{-1}} {latex(A)} {latex(X)} = \\left( {latex(A)} \\right)^{{-1}} {latex(C)}
        $
        <br/>
        <br/>
        $\\displaystyle \\implies 
        {latex(X)} = {latex(A.inv())} {latex(C)}
        $
        <br/>
        <br/>
        $\\displaystyle \\implies 
        {latex(X)} = {latex(A.inv() * C)}
        $
        """
        return soln_s
        pass
    
    def _init_equations(self, A, B, C, X, friend_names, food_names, shop_names):
        
        eqs = [Eq(expr, list(C)[i]) for i, expr in  enumerate(list(A * X))]
        
        eqs_latex = [rf"$\displaystyle {latex(eq)}$" for eq in eqs]
        
        eqs_latex = "<br/>".join(eqs_latex)
        
        result = {
            'eqs': eqs,
            'eqs_latex': eqs_latex,
            }
        return result
        pass
    
    
    
    def _init_qas(self, A, B, C, friend_names, food_names, shop_names):
        qas = []
        common_qas = self._init_common_qas(A, B, C, friend_names, food_names, shop_names)
        qas.extend(common_qas)
        
        if B.shape[1] == 1:
            single_shop_qas = self._init_single_shop_qas(A, B, C, friend_names, food_names, shop_names)
            qas.extend(single_shop_qas)
        else:
            multi_shop_qas = self._init_multi_shop_qas(A, B, C, friend_names, food_names, shop_names)
            qas.extend(multi_shop_qas)

        q_str = [f"{i+1}. {x[0]}" for i, x in enumerate(qas)]
        qa_str = [f"{i+1}. {x[0]}<br/>\t{x[1]}" for i, x in enumerate(qas)]
        
        q_str = "<br/>".join(q_str)
        qa_str = "<br/><br/>".join(qa_str)
    
        result = {
            'qas': qas,
            'q_str': q_str,
            'qa_str': qa_str,
            }
        
        return result
        pass

    def _init_common_qas(self, A, B, C, friend_names, food_names, shop_names):
        qas = []
        qa = [f'How many friend are there?', A.shape[0]]
        qas.append(qa)
        
        qa = [f'How many types of items are ordered?', A.shape[1]]
        qas.append(qa)
        
        qa = [f'How many shops are available for order?', B.shape[1]]
        qas.append(qa)
        
        row_sum_A = (A * sp.ones(A.shape[1], 1)).T
        row_sum_A = np.array(row_sum_A).astype(np.int32)[0]
        row_sum_A_str = [f"{friend_names[i]}: {x}" 
                         for i, x in enumerate(row_sum_A)]
        row_sum_A_str = list_to_str(row_sum_A_str, last_sep = 'and')
        
        qa = [f'What is person wise quantity ordered?', row_sum_A_str]
        qas.append(qa)

        max_row_qty = max(row_sum_A)
        max_qty_friend_idx = list(row_sum_A).index(max_row_qty)
        max_qty_friend = friend_names[max_qty_friend_idx]
        max_qty_friend_str = f"{max_qty_friend}: {max_row_qty}"

        qa = [f'Who ordered maximum quantity?', max_qty_friend_str]
        qas.append(qa)

        min_row_qty = min(row_sum_A)
        min_qty_friend_idx = list(row_sum_A).index(min_row_qty)
        min_qty_friend = friend_names[min_qty_friend_idx]
        min_qty_friend_str = f"{min_qty_friend}: {min_row_qty}"

        qa = [f'Who ordered minimum quantity?', min_qty_friend_str]
        qas.append(qa)
   

        col_sum_A = (A.T * sp.ones(A.T.shape[1], 1)).T
        col_sum_A = np.array(col_sum_A).astype(np.int32)[0]
        col_sum_A_str = [f"{food_names[i]}: {x}" 
                         for i, x in enumerate(col_sum_A)]
        col_sum_A_str = list_to_str(col_sum_A_str, last_sep = 'and')
   
        qa = [f'What is item wise quantity ordered?', col_sum_A_str]
        qas.append(qa)
   
        max_col_qty = max(col_sum_A)
        max_qty_friend_idx = list(col_sum_A).index(max_col_qty)
        max_qty_friend = friend_names[max_qty_friend_idx]
        max_qty_friend_str = f"{max_qty_friend}: {max_col_qty}"

        
        qa = [f'Which item was ordered in maximum quantity?', max_qty_friend_str]
        qas.append(qa)
   
        min_col_qty = min(col_sum_A)
        min_qty_friend_idx = list(col_sum_A).index(min_col_qty)
        min_qty_friend = friend_names[min_qty_friend_idx]
        min_qty_friend_str = f"{min_qty_friend}: {min_col_qty}"

        qa = [f'Which item was ordered in minimum quantity?', min_qty_friend_str]
        qas.append(qa)
   
        qa = [f'What was the total quantity ordered?', sum(A)]
        qas.append(qa)
   
        
        return qas
        pass

    def _init_single_shop_qas(self, A, B, C, friend_names, food_names, shop_names):
        qas = []
        
        idx_friend = random.randint(0, C.shape[0] - 1)
        qa = [f'What is the cost {friend_names[idx_friend]} pays?', C[idx_friend]]
        qas.append(qa)

        qa = [f'What was the total cost?', sum(C)]
        qas.append(qa)
   
        C.shape[0]
        
        cost_list = list(C.T[0, :])
        max_cost = max(cost_list)
        max_cost_friend_idx = cost_list.index(max_cost)
        max_cost_friend = friend_names[max_cost_friend_idx]
        max_cost_friend_str = f"{max_cost_friend}: {max_cost}"

        qa = [f'Whose order has maximum cost?', max_cost_friend_str]
        qas.append(qa)

        min_cost = min(cost_list)
        min_cost_friend_idx = cost_list.index(min_cost)
        min_cost_friend = friend_names[min_cost_friend_idx]
        min_cost_friend_str = f"{min_cost_friend}: {min_cost}"

        qa = [f'Whose order has minimum cost?', min_cost_friend_str]
        qas.append(qa)
        
        return qas
        pass

    def _get_random_shop_data(self, B_org, C_org, shop_names):
        B_idx = random.randint(0, B_org.shape[1] - 1)
        B = B_org[:, B_idx]
        C = C_org[:, B_idx]
        shop_name = shop_names[B_idx]
        return B, C, shop_name

    def _init_multi_shop_qas(self, A, B_org, C_org, friend_names, food_names, shop_names):
        qas = []

        B, C, shop_name = self._get_random_shop_data(B_org, C_org, shop_names)
        
        idx_friend = random.randint(0, C.shape[0] - 1)
        qa = [f'What is the cost {friend_names[idx_friend]} will pay in {shop_name}?', C[idx_friend]]
        qas.append(qa)

        B, C, shop_name = self._get_random_shop_data(B_org, C_org, shop_names)
        
        qa = [f'What was the total cost in {shop_name}?', sum(C)]
        qas.append(qa)
   
        B, C, shop_name = self._get_random_shop_data(B_org, C_org, shop_names)
        
        cost_list = list(C.T[0, :])
        max_cost = max(cost_list)
        max_cost_friend_idx = cost_list.index(max_cost)
        max_cost_friend = friend_names[max_cost_friend_idx]
        max_cost_friend_str = f"{max_cost_friend}: {max_cost}"

        qa = [f'Whose order has maximum cost in {shop_name}?', max_cost_friend_str]
        qas.append(qa)

        B, C, shop_name = self._get_random_shop_data(B_org, C_org, shop_names)

        cost_list = list(C.T[0, :])
        min_cost = min(cost_list)
        min_cost_friend_idx = cost_list.index(min_cost)
        min_cost_friend = friend_names[min_cost_friend_idx]
        min_cost_friend_str = f"{min_cost_friend}: {min_cost}"

        qa = [f'Whose order has minimum cost in {shop_name}?', min_cost_friend_str]
        qas.append(qa)
        
        return qas
        pass

    def get_invert_matrix(self, A, tol=None):
        """
        Returns the inverse of the passed in matrix.
            :param A: The matrix to be inversed
     
            :return: The inverse of the matrix A
        """
        
    def _get_invert_matrix(self, A, tol=None):
        # Section 1: Make sure A can be inverted.
        if A.shape[0] != A.shape[1]:
            sError = f"Error: A is not square matrix: {A.shape}"
            self._log(sError)
            return None
        
        arr1 = np.array(A).astype(np.float64)
        if not (np.linalg.det(arr1)):
            sError = f"Error: A is singular matrix: \n{A}"
            self._log(sError)
            return None
        
        # Section 2: Make copies of A & I, AM & IM, to use for row ops
        n = A.shape[0]
        AM = A.copy() #copy_matrix(A)
        I = Identity(n)
        IM = Matrix(np.array(I).astype(np.float64))
     
        # Section 3: Perform row operations
        indices = list(range(n)) # to allow flexible row referencing ***
    
        soln_s = []
        
        soln_s.append(f"$R$ stands for row.")
        soln_s.append(f"""
        <span style="color: blue;">
        ${{A}}{{I}}\\longrightarrow {latex(AM)}{latex(IM)}$
        </span>
        """.strip())
        for fd in range(n): # fd stands for focus diagonal
            loop_op = False
            if AM[fd, fd] != 1:
                loop_op = True
                soln_item = f"""
                Convert $M[{fd}, {fd}]$ from ${AM[fd, fd]}$ to $1$ 
                <br/>
                To do so, divide each element of $A[{fd}]$ by ${AM[fd, fd]}$
                """
                soln_s.append(soln_item.strip())
                fdScaler = nsimplify(1.0 / AM[fd, fd])
                
                prev_latex = f"""
                {latex(fdScaler)} *
                \\left( 
                {latex(list(AM[fd, :]))} 
                {latex(list(IM[fd, :]))} 
                \\right)
                """.strip()
    
                # FIRST: scale fd row with fd inverse. 
                for j in range(n): # Use j to indicate column looping.
                    AM[fd, j] = nsimplify(AM[fd, j] * fdScaler)
                    IM[fd, j] = nsimplify(IM[fd, j] * fdScaler)
                    pass
    
                soln_item = f"""
                $
                \\begin{{aligned}}
                R{fd} \\rightarrow  
                {latex(fdScaler)} * R{fd} 
                & \\rightarrow {prev_latex} \\\\
                & \\rightarrow {latex(list(AM[fd, :]))} 
                {latex(list(IM[fd, :]))} \\\\
                \\end{{aligned}}
                $
                """
                soln_s.append(soln_item.strip())
                soln_s.append(f"""
                <span style="color: blue;">
                ${{A}}{{I}}\\longrightarrow {latex(AM)}{latex(IM)}$
                </span>
                """.strip())
                pass
            
            # SECOND: operate on all rows except fd row as follows:
            for i in indices[0:fd] + indices[fd+1:]: 
                # *** skip row with fd in it.
                crScaler = AM[i, fd] # cr stands for "current row".
                if crScaler == 0:
                    continue
                
                
                loop_op = True
                
                Ri_latex = f"""
                {latex(list(AM[i, :]))} {latex(list(IM[i, :]))} 
                
                - {latex(crScaler)} *
                \\left ( {latex(list(AM[fd, :]))} {latex(list(IM[fd, :]))} \\right ) 
                """
                
                AM_op_latex = [] 
                IM_op_latex = [] 
                for j in range(n): 
                    # cr - crScaler * fdRow, but one element at a time.
                    AM_prev = AM[i, j]
                    IM_prev = IM[i, j]

                    AM[i, j] = nsimplify(AM_prev - crScaler * AM[fd, j])
                    IM[i, j] = nsimplify(IM_prev - crScaler * IM[fd, j])
                    
                    AM_op_latex.append(f"{latex(AM_prev)} - {latex(crScaler)} * {latex(AM[fd, j])} ")
                    IM_op_latex.append(f"{latex(IM_prev)} - {latex(crScaler)} * {latex(IM[fd, j])} ")
                    
                    from sympy.core.numbers import NaN
                    if isinstance(AM[i, j], NaN):
                        sError = f"Error: NaN encountred"
                        self._log(sError)
                        return None
                    
                    pass
                
                AM_op_latex = ",\quad ".join(AM_op_latex)
                IM_op_latex = ",\quad ".join(IM_op_latex)

                soln_item = f"""
                $
                \\begin{{aligned}}
                R{i} \\rightarrow R{i} - {latex(crScaler)} * R{fd}
                & \\rightarrow {Ri_latex} \\\\
                & \\rightarrow \\left[{AM_op_latex}\\right] \\quad \\left[{IM_op_latex}\\right] \\\\
                & \\rightarrow {latex(list(AM[i, :]))} {latex(list(IM[i, :]))}  \\\\
                \\end{{aligned}}
                $
                
                """
                soln_s.append(soln_item.strip())
        
                pass

            if loop_op:
                soln_s.append(f"""
                <span style="color: blue;">
                ${{A}}{{I}}\\longrightarrow {latex(AM)}{latex(IM)}$
                </span>
                """.strip())
            pass
        
        soln_s.append(f"""
        Hence, inverse of the given matrix is
        <br/>
        <br/>
        ${latex(IM)}$
        """.strip())
        
        soln_s = "<br/><br/>".join(soln_s).strip()
        soln_s = soln_s.replace("\n", "")
        
        result = {
            'A_inv': IM,
            'A_inv_soln_s': soln_s,
            }
        return result
        pass
        
    def _get_row_operations(self, A, X, C):
        # Section 1: Make sure A can be inverted.
        if A.shape[0] != A.shape[1]:
            sError = f"Error: A is not square matrix: {A.shape}"
            self._log(sError)
            return None
        
        arr1 = np.array(A).astype(np.float64)
        if not (np.linalg.det(arr1)):
            sError = f"Error: A is singular matrix: \n{A}"
            self._log(sError)
            return None
        
        # Section 2: Make copies of A & I, AM & IM, to use for row ops
        n = A.shape[0]
        AM = A.copy() #copy_matrix(A)
        IM = C.copy()
     
        # Section 3: Perform row operations
        indices = list(range(n)) # to allow flexible row referencing ***
    
        soln_s = []
        
        soln_s.append(f"""
        $R$ stands for row. 
        The objective of operations is make ${{A}}$ an identity matrix.
        """)
        soln_s.append(f"""
        <span style="color: blue;">
        ${{A}}{{X}} = {{C}}\\longrightarrow {latex(AM)}{latex(X)} = {latex(IM)}$
        </span>
        """.strip())
        for fd in range(n): # fd stands for focus diagonal
            loop_op = False
            if AM[fd, fd] != 1:
                loop_op = True
                soln_item = f"""
                Convert $M[{fd}, {fd}]$ from ${AM[fd, fd]}$ to $1$ 
                <br/>
                To do so, divide each element of $A[{fd}]$ by ${AM[fd, fd]}$
                """
                soln_s.append(soln_item.strip())
                fdScaler = nsimplify(1.0 / AM[fd, fd])
                
                prev_latex = f"""
                {latex(fdScaler)} *
                \\left( 
                {latex(list(AM[fd, :]))} 
                {latex(list(IM[fd, :]))} 
                \\right)
                """.strip()
    
                # FIRST: scale fd row with fd inverse. 
                IM[fd, 0] = nsimplify(IM[fd, 0] * fdScaler)
                for j in range(n): # Use j to indicate column looping.
                    AM[fd, j] = nsimplify(AM[fd, j] * fdScaler)
                    pass
    
                soln_item = f"""
                $
                \\begin{{aligned}}
                R{fd} \\rightarrow  
                {latex(fdScaler)} * R{fd} 
                & \\rightarrow {prev_latex} \\\\
                & \\rightarrow {latex(list(AM[fd, :]))} 
                {latex(list(IM[fd, :]))} \\\\
                \\end{{aligned}}
                $
                """
                soln_s.append(soln_item.strip())
                soln_s.append(f"""
                <span style="color: blue;">
                ${{A}}{{X}} = {{C}}\\longrightarrow {latex(AM)}{latex(X)} = {latex(IM)}$
                </span>
                """.strip())
                pass
            
            # SECOND: operate on all rows except fd row as follows:
            for i in indices[0:fd] + indices[fd+1:]: 
                # *** skip row with fd in it.
                crScaler = AM[i, fd] # cr stands for "current row".
                if crScaler == 0:
                    continue
                loop_op = True
                
                Ri_latex = f"""
                {latex(list(AM[i, :]))} {latex(list(IM[i, :]))} 
                
                - {latex(crScaler)} *
                \\left ( {latex(list(AM[fd, :]))} {latex(list(IM[fd, :]))} \\right ) 
                """
                
                AM_op_latex = [] 
                IM_op_latex = [] 

                IM_prev = IM[i, 0]

                IM[i, 0] = nsimplify(IM_prev - crScaler * IM[fd, 0])
                IM_op_latex.append(f"{latex(IM_prev)} - {latex(crScaler)} * {latex(IM[fd, 0])} ")

                for j in range(n): 

                    AM_prev = AM[i, j]

                    # cr - crScaler * fdRow, but one element at a time.
                    AM[i, j] = nsimplify(AM_prev - crScaler * AM[fd, j])
                    AM_op_latex.append(f"{latex(AM_prev)} - {latex(crScaler)} * {latex(AM[fd, j])} ")
                    
                    
                    from sympy.core.numbers import NaN
                    if isinstance(AM[i, j], NaN):
                        sError = f"Error: NaN encountred"
                        self._log(sError)
                        return None
                    
                    pass

                AM_op_latex = ",\quad ".join(AM_op_latex)
                IM_op_latex = ",\quad ".join(IM_op_latex)

                soln_item = f"""
                $
                \\begin{{aligned}}
                R{i} \\rightarrow R{i} - {latex(crScaler)} * R{fd}
                & \\rightarrow {Ri_latex} \\\\
                & \\rightarrow \\left[{AM_op_latex}\\right] \\quad \\left[{IM_op_latex}\\right] \\\\
                & \\rightarrow {latex(list(AM[i, :]))} {latex(list(IM[i, :]))}  \\\\
                \\end{{aligned}}
                $
                
                """
                soln_s.append(soln_item.strip())
        
                pass
            
            if loop_op:
                soln_s.append(f"""
                <span style="color: blue;">
                ${{A}}{{X}} = {{C}}\\longrightarrow {latex(AM)}{latex(X)} = {latex(IM)}$
                </span>
                """.strip())
            pass
        
        soln_s.append(f"""
        Hence, the solution is
        <br/>
        <br/>
        ${latex(X)} = {latex(IM)}$
        """.strip())
        
        soln_s = "<br/><br/>".join(soln_s).strip()
        soln_s = soln_s.replace("\n", "")
        
        result = {
            'row_operations': soln_s,
            }
        return result
        pass
        
        
    def get_problem_solve_a_determinant(self):
        
        row_count = random.randint(2, 4)
        col_count = random.randint(2, 4)
        shape = (row_count, random.choice([row_count, col_count]))
        
        mtrx = self._getRandomMatrix(shape)
        result = self.get_solution_of_determinant(mtrx)
        
        return result
        pass
    
    def get_solution_of_determinant(self, mtrx):
        
        prob_s = f"""
        Compute determinant of the matrix:
        $\\displaystyle {latex(mtrx)}$
        """
        
        soln_s, determ_val = self._get_solution_of_determinant(mtrx)
        
        answer_s = f"""
        $\\displaystyle {latex(determ_val)}$
        """
    
        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        return result
        pass

    def _get_solution_of_determinant(self, mtrx):
        
        shape = sp.shape(mtrx)

        soln_stmts = []
        soln_stmt = f"""
        The shape of the $\\displaystyle {latex(mtrx)}
        \\quad \\text{{ is }} {latex(shape)}$.
        """
        soln_stmts.append(soln_stmt.strip())

        if shape[0] != shape[1]:
            soln_stmt = f"""
            The row and column counts of the matrix are not equal,i.e.,
            it is not a square matrix. 
            Therefore, its determinant cannot be evaluated.
            """
            soln_stmt = soln_stmt.replace("\n", " ").strip()
            soln_s = "\n\n".join(soln_stmts)
            return soln_s, None
        
        
        soln_stmt = f"""
        The row and column counts of the matrix are equal,i.e.,
        it is a square matrix. 
        Therefore, its determinant can be evaluated.
        """
        soln_stmts.append(soln_stmt.strip())
        
        mtrx_symb = Symbol('M')
        determ_symb = Symbol('D')
        
        determ = self._get_determinant_symb(mtrx)
        
        determ_val = mtrx.det()
        
        soln_stmt = f"""
        $\\displaystyle 
        \\begin {{aligned}}
        
        {determ_symb}
        & = \\text{{det}} \\left( {latex(mtrx_symb)} \\right) 
        \\\\ \\\\
        
        & = \\text{{det}} \\left( {latex(mtrx)} \\right) 
        \\\\ \\\\
        
        & = {latex(determ)}
        \\qquad \\text{{ this is the standard way a determinant is written.}}
        \\\\ \\\\
        """
        soln_stmts.append(soln_stmt.strip())

        sub_matrxs = self._get_solution_step_of_determinant(mtrx, soln_stmts)
        
        soln_stmt = f"""
        
        & = {latex(determ_val)}
        
        \\end {{aligned}}
        $

        """
        soln_stmts.append(soln_stmt.strip())
        
        if sub_matrxs:
            soln_stmt = f"""
            <h3>Detailed Solution:</h3>
            """
            soln_stmts.append(soln_stmt.strip())
            
            for sub_matrx in sub_matrxs:
                sub_determ = self._get_determinant_symb(sub_matrx)
                sub_determ_val = sub_matrx.det()

                sub_soln_stmts = []
                
                sub_soln_stmt = f"""
                $\\displaystyle 
                \\begin {{aligned}}
                
                & \\large {{ {latex(sub_determ)} }}
                \\\\ \\\\
                """
                sub_soln_stmts.append(sub_soln_stmt.strip())
        
                self._get_solution_step_of_determinant(sub_matrx, sub_soln_stmts)
                
                sub_soln_stmt = f"""
                
                & = {latex(sub_determ_val)}
                
                \\end {{aligned}}
                $
                <hr/>
                """
                sub_soln_stmts.append(sub_soln_stmt.strip())
        

                soln_stmts.extend(sub_soln_stmts)
                pass
        
        
        soln_s = "\n\n".join(soln_stmts)
        return soln_s, determ_val
        pass

    def _get_solution_step_of_determinant(self, mtrx, soln_stmts):
        '''
        it returns latex with alignment
        '''
        
        shape = sp.shape(mtrx)
        M = mtrx.copy().tolist()

        if shape[0] < 2:
            return None
      
        if shape[0] == 2:
            soln_stmt = f"""
            & = {latex(M[0][0])} \\cdot {latex(M[1][1])}
               \\; - \\; 
               {latex(M[1][0])} \\cdot {latex(M[0][1])}
            \\qquad \\text{{ cross multiply and subtract the first from the second }}
            \\\\ \\\\
            
            & = {latex(M[0][0] * M[1][1])}
              \\; - \\; 
              {latex(M[1][0] * M[0][1])}
            \\\\ \\\\
            
            & = {latex(Matrix(M).det())}
            \\\\ \\\\
            """
            soln_stmts.append(soln_stmt.strip())
            
            return []
            pass
        
        sub_matrxs = self._split_determinant(mtrx, soln_stmts)
        return sub_matrxs
        pass

    def _split_determinant(self, mtrx, soln_stmts):
        det = 0

        sum_struck_soln_stmts = []
        sum_ueval_soln_stmts = []
        
        sum_ueval_2_by_2_stmts = []
        
        sum_soln_stmts = []
        
        
        sub_matrxs = []
        for j in range(mtrx.shape[1]):
            
            sub_matrx_struck = strike_matrix_to_determinant(mtrx, j,
                                                            anchor_color = "red", 
                                                            strike_color = 'lightblue')
            
            anchor_elem = mtrx[0, j]
            anchor_elem_colored = latex_color(latex(anchor_elem), "red")
            
            sub_matrx = mtrx.minor_submatrix(0, j)
            
            sub_determ_symb = self._get_determinant_symb(sub_matrx)
            
            sub_determ_val = (-1)**(0+j) * anchor_elem * sub_matrx.det()
            
            det = det + sub_determ_val
            
            soln_stmt = f"""
            {{ (-1) }}^{{ {(0+j)} }} 
            \\cdot {anchor_elem_colored}
            \\cdot {latex(sub_matrx_struck)}
            """
            sum_struck_soln_stmts.append(soln_stmt.strip())
            
            soln_stmt = f"""
            {{ (-1) }}^{{ {(0+j)} }} 
            \\cdot {anchor_elem_colored}
            \\cdot {latex(sub_determ_symb)}
            """
            sum_ueval_soln_stmts.append(soln_stmt.strip())
            
            if sp.shape(sub_matrx)[0] == 2:
                expn_matrx = self._get_2_by_2_determinant_expansion(sub_matrx)
                soln_stmt = f"""
                {{ (-1) }}^{{ {(0+j)} }} 
                \\cdot {anchor_elem_colored}
                \\cdot \\left( {latex(expn_matrx)} \\right)
                """
                sum_ueval_2_by_2_stmts.append(soln_stmt)
                pass
            
            soln_stmt = f"""
            {latex(sub_determ_val)}
            """
            sum_soln_stmts.append(soln_stmt.strip())
            
            
            sub_matrxs.append(sub_matrx)
            pass
        
        sum_struck_soln_s = " \\; + \\; ".join(sum_struck_soln_stmts)
        soln_stmts.append(f"& = {sum_struck_soln_s} \\\\ \\\\ ")

        sum_ueval_soln_s = " \\; + \\; ".join(sum_ueval_soln_stmts)
        soln_stmts.append(f"& = {sum_ueval_soln_s} \\\\ \\\\ ")
        
        if sum_ueval_2_by_2_stmts:
            sum_ueval_2_by_2_s = " \\; + \\; ".join(sum_ueval_2_by_2_stmts)
            soln_stmts.append(f"& = {sum_ueval_2_by_2_s} \\\\ \\\\ ")
        

        sum_soln_s = " \\; + \\; ".join(sum_soln_stmts)
        soln_stmts.append(f"& = {sum_soln_s} \\\\ \\\\ ")
        
        return sub_matrxs
        pass
    
    def _get_determinant_symb(self, mtrx):
        determ_latex = latex(mtrx).replace("[", "|").replace("]", "|")
        determ_symb = Symbol(f""" {determ_latex} """)
        return determ_symb

    def _get_2_by_2_determinant_expansion(self, mtrx, color = 'blue'):
        
        shape = sp.shape(mtrx)
        M = mtrx.copy().tolist()

        if shape[0] != 2:
            return None
      
        expn_ltx = f"""
        {latex(M[0][0])} \\cdot {latex(M[1][1])}
           \\; - \\; 
           {latex(M[1][0])} \\cdot {latex(M[0][1])}
        """
        expn_ltx = latex_color(expn_ltx, color)
        
        expn_sym = Symbol(expn_ltx)
        return expn_sym


    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

    pass
#class MatrixHelper:
