'''
Created on 12-Jul-2021

@author: bthakur

'''
import inspect
import traceback

import random
from xv.km.util import latex_fraction, latex_color
from xv.util import toIntOrFloat

from sympy import (Rational, sqrt, srepr, symbols, Symbol, limit, latex, Function,
                   Limit, oo, Derivative, Integral,
                   simplify, solve, expand,
                   exp, log, expand_log,
                   Mul, Add, Pow,
                   Sum, binomial, factorial, 
                   factor, sympify,
                   Eq, srepr
                   )

import sympy

from sympy import UnevaluatedExpr as ueval

from sympy import (acos, acot, acoth, acsc, asec, asin, 
                   asinh, atan, atan2, atanh, 
                   cos, cosh, cot, coth, csc, sec, 
                   sin, sinc, sinh, tan, tanh)

from ._derivative_solver import DerivativeSolver

from xv.util import run_with_timeout

# integration_helper
# IntegrationHelper

from xv.util import UtilClass

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

        self.a = symbols('a', constant = True) #, nonzero=True)
        self.b = symbols('b', constant = True)
        self.c = symbols('c', constant = True)
        self.n = symbols('n', constant = True)
        self.m = symbols('m', constant = True)
        
        self.u = symbols('u')
        self.x = symbols('x')
        self.y = Function('y')(self.x)
        self.z = symbols('z')
        
        pass

    def get_solution(self, keyword = None):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        if keyword is None:
            return self._get_solution_standard()
        
        if keyword.replace(" ", "") == "trig":
            return self._get_solution_trig()
        
        if keyword == "by_var_change":
            return self._get_solution_by_var_change()
        
        if keyword == "by_transform":
            return self._get_solution_by_transform()
        
        return self._get_solution_standard()
        pass

    def get_random_exprs(self, keyword = None):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        x = self.x
        
        x0, x1 = None, None
        if random.choice([True, False]):
            while x0 == x1:
                x0 = random.choice([self.a, random.randint(1, 10)])
                x1 = random.choice([self.b, random.randint(1, 10)])
            
        n0 = random.choice([self.n, random.randint(1, 10)])
        a = self.a
        b = self.b
        n = self.n
        
        exprs = []
        
        expr = x ** n0
        exprs.append([expr, x, x0, x1])

        expr = exp(x)
        exprs.append([expr, x, x0, x1])

        expr = n0 ** x
        exprs.append([expr, x, x0, x1])
        
        expr = 1 / x
        exprs.append([expr, x, x0, x1])
        
        expr = sin(x)
        exprs.append([expr, x, x0, x1])
        
        expr = cos(x)
        exprs.append([expr, x, x0, x1])
        
        expr = sec(x) ** 2
        exprs.append([expr, x, x0, x1])
        
        expr = csc(x) ** 2
        exprs.append([expr, x, x0, x1])
        
        expr = sec(x) * tan(x)
        exprs.append([expr, x, x0, x1])
        
        expr = csc(x) * cot(x)
        exprs.append([expr, x, x0, x1])
        
        # problem in limit solution
        expr = cot(x)
        exprs.append([expr, x, x0, x1])
        
        expr = tan(x)
        exprs.append([expr, x, x0, x1])
        
        # problem in solution expected: log(sec(x) + tan(x))
        expr = sec(x)
        exprs.append([expr, x, x0, x1])
        
        # problem in solution expected: log(csc(x) - cot(x))
        expr = csc(x)
        exprs.append([expr, x, x0, x1])
        
        expr = 1 / sqrt(a ** 2 - x ** 2)
        exprs.append([expr, x, x0, x1])
        
        return exprs
        pass
    
    def get_random_expr(self, keyword = None):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        exprs = self.get_random_exprs(keyword = keyword)
        return random.choice(exprs[-1:])
        # return random.choice(exprs)
        pass
    
    def get_random_expr_ax_plus_b(self):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        x = self.x
        
        x0, x1 = None, None
        if random.choice([True, False]):
            while x0 == x1:
                x0 = random.randint(-10, 10)
                x1 = random.randint(-10, 10)
            
        n0 = random.choice([random.randint(-9, -1), random.randint(1, 10)])
        a = random.choice([random.randint(-9, -1), random.randint(1, 10)])
        b = random.choice([random.randint(-9, -1), random.randint(1, 10)])
        n = random.choice([random.randint(-9, -1), random.randint(1, 10)])
        
        subs_expr = a * x + b
        
        exprs = []
        
        random_exprs = self.get_random_exprs()
        random_exprs = [[expr.subs(x, subs_expr), x, x0, x1] 
                        for expr, x, x0, x1 in random_exprs]
        
        exprs.extend(random_exprs)

        #return random.choice(exprs[-3:]), subs_expr
        return random.choice(exprs), subs_expr
        pass


    def get_random_expr_by_var_change(self, keyword = None):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        exprs = self.get_random_exprs_by_var_change(keyword = keyword)
        return random.choice(exprs)

    def get_random_exprs_by_var_change(self, keyword = None):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        x = self.x
        pi = sympy.pi
        
        x0, x1 = None, None
        if random.choice([True, False]):
            while x0 == x1:
                x0 = random.choice([self.a, 0, pi/4, pi/2, pi, 2 * pi, 4 * pi])
                x1 = random.choice([self.b, 0, pi/4, pi/2, pi, 2 * pi, 4 * pi])
                if random.choice([True, False]):
                    x0 = -x0
                if random.choice([True, False]):
                    x1 = -x1
            
        m0, n0 = None, None
        while m0 == n0:
            m0 = random.choice([self.m, random.randint(1, 10)])
            n0 = random.choice([self.n, random.randint(1, 10)])
        
        
        a = self.a
        b = self.b
        m = self.m
        n = self.n
        
        exprs = []
        
        
        random_exprs = self.get_random_exprs()
        subs_expr = a * x + b
        u = self.u
        subs_tuple = (subs_expr, u)
        random_exprs = [[expr.subs(x, subs_expr), x, x0, x1, subs_tuple] 
                        for expr, x, x0, x1 in random_exprs]
        
        exprs.extend(random_exprs)

        temp_expr = a ** 2 + x ** 2
        expr = temp_expr
        subs_tuple = (x, random.choice([a * tan(self.u), a * cot(self.u), a * sinh(self.u)]))
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = 1/sqrt(temp_expr)
        subs_tuple = (x, a * tan(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = 1/sqrt(temp_expr)
        subs_tuple = (x, a * cot(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = 1/sqrt(temp_expr)
        subs_tuple = (x, a * sinh(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(temp_expr)
        subs_tuple = (x, a * tan(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(temp_expr)
        subs_tuple = (x, a * cot(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = sqrt(temp_expr)
        subs_tuple = (x, a * sinh(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])

        temp_expr = a ** 2 - x ** 2
        expr = temp_expr
        subs_tuple = (x, random.choice([a * sin(self.u), a * cos(self.u)]))
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = 1/sqrt(temp_expr)
        subs_tuple = (x, random.choice([a * sin(self.u), a * cos(self.u)]))
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = sqrt(temp_expr)
        subs_tuple = (x, random.choice([a * sin(self.u), a * cos(self.u)]))
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = 2 * x / a ** 2 - x ** 2
        subs_tuple = (x, a * tan(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])
        
        expr = 2 * x / a ** 2 + x ** 2
        subs_tuple = (x, a * tan(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])
        
        expr = (a ** 2 - x ** 2) / (a ** 2 + x ** 2)
        subs_tuple = (x, a * tan(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])
        
        temp_expr = x ** 2 - a ** 2
        expr = temp_expr
        subs_tuple = (x, random.choice([a * sec(self.u), a * csc(self.u), a * cosh(self.u)]))
        exprs.append([expr, x, x0, x1, subs_tuple])
        
        expr = sqrt(temp_expr)
        subs_tuple = (x, random.choice([a * sec(self.u), a * cosh(self.u)]))
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(temp_expr)
        subs_tuple = (x, a * csc(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])
        
        # TODO - notworking
        expr = 1/sqrt(temp_expr)
        subs_tuple = (x, a * sec(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = 1/sqrt(temp_expr)
        subs_tuple = (x, a * csc(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = 1/sqrt(temp_expr)
        subs_tuple = (x, a * cosh(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt((a - x) / (a + x))
        subs_tuple = (x, a * cos(2*self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt((a + x) / (a - x))
        subs_tuple = (x, a * cos(2*self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt((x - a) / (b - x))
        subs_tuple = (x, a * cos(self.u) ** 2 + b * sin(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt((x - a) * (b - x))
        subs_tuple = (x, a * cos(self.u) ** 2 + b * sin(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = sqrt(2 * a * x - x ** 2)
        subs_tuple = (x, a * (1 - cos(self.u)))
        exprs.append([expr, x, x0, x1, subs_tuple])

        temp_expr = a + x
        # TODO - notworking
        expr = sqrt(x / temp_expr)
        subs_tuple = (x, a * tan(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(x / temp_expr)
        subs_tuple = (x, a * cot(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(temp_expr / x)
        subs_tuple = (x, a * tan(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(temp_expr / x)
        subs_tuple = (x, a * cot(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(x * temp_expr)
        subs_tuple = (x, a * tan(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(x * temp_expr)
        subs_tuple = (x, a * cot(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        temp_expr = a - x
        # TODO - notworking
        expr = sqrt(x / temp_expr)
        subs_tuple = (x, a * sin(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(x / temp_expr)
        subs_tuple = (x, a * cos(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(temp_expr / x)
        subs_tuple = (x, a * sin(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(temp_expr / x)
        subs_tuple = (x, a * cos(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(x * temp_expr)
        subs_tuple = (x, a * sin(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(x * temp_expr)
        subs_tuple = (x, a * cos(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = 1 / sqrt(x * temp_expr)
        subs_tuple = (x, a * sin(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = 1 / sqrt(x * temp_expr)
        subs_tuple = (x, a * cos(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        temp_expr = x - a
        # TODO - notworking
        expr = sqrt(x / temp_expr)
        subs_tuple = (x, a * sec(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(x / temp_expr)
        subs_tuple = (x, a * csc(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(temp_expr / x)
        subs_tuple = (x, a * sec(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(temp_expr / x)
        subs_tuple = (x, a * csc(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(x * temp_expr)
        subs_tuple = (x, a * sec(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = sqrt(x * temp_expr)
        subs_tuple = (x, a * csc(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = 1 / sqrt(x * temp_expr)
        subs_tuple = (x, a * sec(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        # TODO - notworking
        expr = 1 / sqrt(x * temp_expr)
        subs_tuple = (x, a * csc(self.u) ** 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        exprs = [[expr_data[:-1], expr_data[-1]] 
                 for expr_data in exprs]

        return exprs
        pass


    def get_random_expr_by_transform(self, keyword = None):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        exprs = self.get_random_exprs_by_transform(keyword = keyword)
        return random.choice(exprs)

    def get_random_exprs_by_transform(self, keyword = None):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        x = self.x
        pi = sympy.pi
        
        x0, x1 = None, None
        if random.choice([True, False]):
            while x0 == x1:
                x0 = random.choice([self.a, 0, pi/4, pi/2, pi, 2 * pi, 4 * pi])
                x1 = random.choice([self.b, 0, pi/4, pi/2, pi, 2 * pi, 4 * pi])
                if random.choice([True, False]):
                    x0 = -x0
                if random.choice([True, False]):
                    x1 = -x1
            
        m0, n0 = None, None
        while m0 == n0:
            m0 = random.choice([self.m, random.randint(1, 10)])
            n0 = random.choice([self.n, random.randint(1, 10)])
        
        
        a = self.a
        b = self.b
        m = self.m
        n = self.n
        c = self.c
        
        exprs = []

        temp_expr = a * x** 2 + b * x + c
        expr = sqrt(temp_expr)
        subs_tuple = (x, a * sinh(self.u))
        exprs.append([expr, x, x0, x1, subs_tuple])



        exprs = [[expr_data[:-1], expr_data[-1]] 
                 for expr_data in exprs]
        return exprs
        pass

    def get_random_expr_trig(self, keyword = None):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        exprs = self.get_random_exprs_trig(keyword = keyword)
        return random.choice(exprs)

    def get_random_exprs_trig(self, keyword = None):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        x = self.x
        pi = sympy.pi
        
        x0, x1 = None, None
        if random.choice([True, False]):
            while x0 == x1:
                x0 = random.choice([self.a, 0, pi/4, pi/2, pi, 2 * pi, 4 * pi])
                x1 = random.choice([self.b, 0, pi/4, pi/2, pi, 2 * pi, 4 * pi])
                if random.choice([True, False]):
                    x0 = -x0
                if random.choice([True, False]):
                    x1 = -x1
            
        m0, n0 = None, None
        while m0 == n0:
            m0 = random.choice([self.m, random.randint(1, 10)])
            n0 = random.choice([self.n, random.randint(1, 10)])
        
        
        a = self.a
        b = self.b
        m = self.m
        n = self.n
        
        exprs = []
        
        expr = sin(n * x) ** 2
        subs_tuple = (expr, (1 - cos(2 * n * x)) / 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = cos(n * x) ** 2
        subs_tuple = (expr, (1 + cos(2 * n * x)) / 2)
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = sin(n * x)
        subs_tuple = (n * x, n * x)
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = sin(n * x)
        subs_tuple = (sin(n * x), 2 * sin(n * x / 2) * cos(n * x / 2))
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = sin(n * x) * cos(n * x)
        subs_tuple = (expr, sin(2 * n * x) / 2)
        exprs.append([expr, x, x0, x1, subs_tuple])



        expr = sin(n * x) ** 3
        subs_tuple = (expr, Rational(3, 4) * sin(n * x) - Rational(1, 4) * sin(3 * n * x) )
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = cos(n * x) ** 3
        subs_tuple = (expr, Rational(3, 4) * cos(n * x) + Rational(1, 4) * cos(3 * n * x) )
        exprs.append([expr, x, x0, x1, subs_tuple])


        m0x = ueval(m0 * x)
        n0x = ueval(n0 * x)

        expr = sin(m0x) * cos(n0x)
        expanded_expr = sin(m0x + n0x) + sin(m0x - n0x)
        subs_tuple = (expr, Rational(1, 2) * expanded_expr)
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = cos(m0x) * sin(n0x)
        expanded_expr = sin(m0x + n0x) - sin(m0x - n0x)
        subs_tuple = (expr, Rational(1, 2) * expanded_expr)
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = cos(m0x) * cos(n0x)
        expanded_expr = cos(m0x - n0x) + cos(m0x + n0x)
        subs_tuple = (expr, Rational(1, 2) * expanded_expr)
        exprs.append([expr, x, x0, x1, subs_tuple])

        expr = sin(m0x) * sin(n0x)
        expanded_expr = cos(m0x - n0x) - cos(m0x + n0x)
        subs_tuple = (expr, Rational(1, 2) * expanded_expr)
        exprs.append([expr, x, x0, x1, subs_tuple])

        
        exprs = [[expr_data[:-1], expr_data[-1]] 
                 for expr_data in exprs]

        return exprs
        pass

    def _get_solution_by_var_change(self):

        exprs = self.get_random_exprs_by_var_change(keyword = None)
        
        exprs_str = [f"$\\displaystyle  {latex(item[0][0])}$" 
                     for item in exprs]
        
        exprs_str = "\n\n".join(list(set(exprs_str)))
        
        (expr, x, x0, x1), subs_tuple = random.choice(exprs)

        problem_s = self._get_problem_s(expr, x, x0, x1)
        
        I0 = self._Integral(expr, x, x0, x1)
            
        soln_items = []
        
        soln_item = f"""
        $\\displaystyle 
        {latex(I0)}
        $
        """
        soln_items.append(soln_item.strip())

        a = self.a
        b = self.b
        u = self.u
        u_eq = Eq(subs_tuple[1], subs_tuple[0])

        x_expr = solve(u_eq, x)[0]
        
        dx = Symbol(f"d{latex(x)}")
        du = Symbol(f"d{latex(u)}")
        
        x_eq = Eq(x, x_expr)
        
        dx_du_lhs = Derivative(x_eq.lhs, x).doit()
        dx_du_rhs = Derivative(x_eq.rhs, u).doit()
 
        if dx_du_rhs == 1:
            dx_du_eq = Eq(dx_du_lhs * dx, du)
        else:
            dx_du_eq = Eq(dx_du_lhs * dx, ueval(dx_du_rhs) * ueval(du))
            
        
        soln_item = f"""
        Let: 
        
        $\\displaystyle 
        {latex(u_eq)}
        $
        
        $\\displaystyle 
        {latex(x_eq)}
        $
        
        $\\displaystyle 
        \\implies {latex(dx_du_eq)}
        $
        
        """
        soln_items.append(soln_item.strip())


        if (x0 is None or x1 is None):
            u0, u1 = None, None
        else:
            u0_symb = Symbol('u_0')
            u1_symb = Symbol('u_1')
            
            u0 = u_eq.rhs.subs(x, x0)
            u1 = u_eq.rhs.subs(x, x1)
            
            soln_item = f"""
            Also, 
            
            $\\displaystyle 
            {latex(u0_symb)} 
            = \\left . {latex(u_eq.rhs)} \\right | _{{{latex(x)} = {latex(x0)}}}
            = {latex(u0)}
            $
            
            $\\displaystyle 
            {latex(u1_symb)} 
            = \\left . {latex(u_eq.rhs)} \\right | _{{{latex(x)} = {latex(x1)}}}
            = {latex(u1)}
            $
            
            """
            soln_items.append(soln_item.strip())
    
            pass

        expr_subs = expr.subs(u_eq.rhs, u_eq.lhs).subs(x_eq.lhs, x_eq.rhs)
        
        if dx_du_rhs == 1:
            mod_expr_ueval = ueval(expr_subs)
        else:
            mod_expr_ueval = ueval(expr_subs) * ueval(dx_du_rhs)
            
        mod_expr = simplify(mod_expr_ueval)

        I0_mod_ueval = self._Integral(mod_expr_ueval, u, u0, u1)
        I0_mod = self._Integral(mod_expr, u, u0, u1)
        
        soln_item = f"""
        Therefore, 
        
        $\\displaystyle 
        {latex(I0)}
        $
        
        $\\displaystyle 
        = {latex(I0_mod_ueval)}
        $
        
        $\\displaystyle 
        = {latex(I0_mod)}
        $
        """
        soln_items.append(soln_item.strip())

        
        I_stmts, I_soln = self._Integral_doit(mod_expr, u, u0, u1)
        I_stmts, I_soln = self._Integral_doit(mod_expr, u, u0, u1)
        soln_items.extend(I_stmts[1:])


        
        soln_item = f"""
        <h2>List of other expressions:</h2>
        {exprs_str}
        
        """
        soln_items.append(soln_item.strip())

        
        
        soln_s = "\n\n".join(soln_items)
        
        answer_s = f"""
        $\\displaystyle {latex(I_soln)} $
        """
        
        result = {
            'expr': expr,
            'x': x,
            'x0': x0,
            'x1': x1,
            'I_soln': I_soln,
            'problem_s': problem_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass

    def _get_solution_by_transform(self):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)


        exprs = self.get_random_exprs_by_var_change(keyword = None)
        
        exprs_str = [f"$\\displaystyle  {latex(item[0][0])}$" 
                     for item in exprs]
        
        exprs_str = "\n\n".join(list(set(exprs_str)))
        
        (expr, x, x0, x1), subs_tuple = random.choice(exprs)

        problem_s = self._get_problem_s(expr, x, x0, x1)
        
        I0 = self._Integral(expr, x, x0, x1)
            
        soln_items = []
        
        soln_item = f"""
        $\\displaystyle 
        {latex(I0)}
        $
        """
        soln_items.append(soln_item.strip())

        a = self.a
        b = self.b
        u = self.u
        u_eq = Eq(subs_tuple[1], subs_tuple[0])

        x_expr = solve(u_eq, x)[0]
        
        dx = Symbol(f"d{latex(x)}")
        du = Symbol(f"d{latex(u)}")
        
        x_eq = Eq(x, x_expr)
        
        dx_du_lhs = Derivative(x_eq.lhs, x).doit()
        dx_du_rhs = Derivative(x_eq.rhs, u).doit()
 
        if dx_du_rhs == 1:
            dx_du_eq = Eq(dx_du_lhs * dx, du)
        else:
            dx_du_eq = Eq(dx_du_lhs * dx, ueval(dx_du_rhs) * ueval(du))
            
        
        soln_item = f"""
        Let: 
        
        $\\displaystyle 
        {latex(u_eq)}
        $
        
        $\\displaystyle 
        {latex(x_eq)}
        $
        
        $\\displaystyle 
        \\implies {latex(dx_du_eq)}
        $
        
        """
        soln_items.append(soln_item.strip())


        if (x0 is None or x1 is None):
            u0, u1 = None, None
        else:
            u0_symb = Symbol('u_0')
            u1_symb = Symbol('u_1')
            
            u0 = u_eq.rhs.subs(x, x0)
            u1 = u_eq.rhs.subs(x, x1)
            
            soln_item = f"""
            Also, 
            
            $\\displaystyle 
            {latex(u0_symb)} 
            = \\left . {latex(u_eq.rhs)} \\right | _{{{latex(x)} = {latex(x0)}}}
            = {latex(u0)}
            $
            
            $\\displaystyle 
            {latex(u1_symb)} 
            = \\left . {latex(u_eq.rhs)} \\right | _{{{latex(x)} = {latex(x1)}}}
            = {latex(u1)}
            $
            
            """
            soln_items.append(soln_item.strip())
    
            pass

        expr_subs = expr.subs(u_eq.rhs, u_eq.lhs).subs(x_eq.lhs, x_eq.rhs)
        
        if dx_du_rhs == 1:
            mod_expr_ueval = ueval(expr_subs)
        else:
            mod_expr_ueval = ueval(expr_subs) * ueval(dx_du_rhs)
            
        mod_expr = simplify(mod_expr_ueval)

        I0_mod_ueval = self._Integral(mod_expr_ueval, u, u0, u1)
        I0_mod = self._Integral(mod_expr, u, u0, u1)
        
        soln_item = f"""
        Therefore, 
        
        $\\displaystyle 
        {latex(I0)}
        $
        
        $\\displaystyle 
        = {latex(I0_mod_ueval)}
        $
        
        $\\displaystyle 
        = {latex(I0_mod)}
        $
        """
        soln_items.append(soln_item.strip())

        
        I_stmts, I_soln = self._Integral_doit(mod_expr, u, u0, u1)
        I_stmts, I_soln = self._Integral_doit(mod_expr, u, u0, u1)
        soln_items.extend(I_stmts[1:])


        
        soln_item = f"""
        <h2>List of other expressions:</h2>
        {exprs_str}
        
        """
        soln_items.append(soln_item.strip())

        
        
        soln_s = "\n\n".join(soln_items)
        
        answer_s = f"""
        $\\displaystyle {latex(I_soln)} $
        """
        
        result = {
            'expr': expr,
            'x': x,
            'x0': x0,
            'x1': x1,
            'I_soln': I_soln,
            'problem_s': problem_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass

    def _get_solution_trig(self):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)


        exprs = self.get_random_exprs_trig(keyword = None)
        
        exprs_str = [f"$\\displaystyle  {latex(item[0][0])}$" 
                     for item in exprs]
        
        exprs_str = "\n\n".join(list(set(exprs_str)))
        
        identifier = f"{self.__class__.__name__}{id(self)}.{inspect.stack()[0][3]}.random_exprs"
        random_expr = self.get_no_repeat_random_choice(identifier, exprs)
        
        (expr, x, x0, x1), subs_tuple = random_expr

        problem_s = self._get_problem_s(expr, x, x0, x1)
        
        I0 = self._Integral(expr, x, x0, x1)
            
        soln_items = []
        
        soln_item = f"""
        $\\displaystyle 
        {latex(I0)}
        $
        """
        soln_items.append(soln_item.strip())
        
        
        soln_item = f"""
        As we know: 
        
        $\\displaystyle 
        {latex(subs_tuple[0])} = {latex(subs_tuple[1])}
        $
        
        """
        soln_items.append(soln_item.strip())



        mod_expr_ueval = expr.subs(subs_tuple[0], ueval(subs_tuple[1]))
        mod_expr = expand(mod_expr_ueval)
        
        I0_mod_ueval = self._Integral(mod_expr_ueval, x, x0, x1)
        I0_mod = self._Integral(mod_expr, x, x0, x1)
        
        soln_item = f"""
        Therefore, 
        
        $\\displaystyle 
        {latex(I0)}
        $
        
        $\\displaystyle 
        = {latex(I0_mod_ueval)}
        $
        
        $\\displaystyle 
        = {latex(I0_mod)}
        $
        """
        soln_items.append(soln_item.strip())

        
        I_stmts, I_soln = self._Integral_doit(mod_expr, x, x0, x1)
        soln_items.extend(I_stmts[1:])

        soln_item = f"""
        <h2>List of other expressions:</h2>
        {exprs_str}
        
        """
        soln_items.append(soln_item.strip())

        
        soln_s = "\n\n".join(soln_items)
        
        answer_s = f"""
        $\\displaystyle {latex(I_soln)} $
        """
        
        result = {
            'expr': expr,
            'x': x,
            'x0': x0,
            'x1': x1,
            'I_soln': I_soln,
            'problem_s': problem_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass

    def _get_solution_standard(self):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)


        expr, x, x0, x1 = self.get_random_expr()

        problem_s = self._get_problem_s(expr, x, x0, x1)
        
        I0 = self._Integral(expr, x, x0, x1)
            
        soln_items = []
        
        soln_item = f"""
        $\\displaystyle 
        {latex(I0)}
        $
        """
        soln_items.append(soln_item.strip())
        
        I_stmts, I_soln = self._get_soln_by_expr_type(expr, x, x0, x1)
        if I_stmts:
            soln_items.extend(I_stmts)

        
        soln_s = "\n\n".join(soln_items)
        
        answer_s = f"""
        $\\displaystyle {latex(I_soln)} $
        """
        
        result = {
            'expr': expr,
            'x': x,
            'x0': x0,
            'x1': x1,
            'I_soln': I_soln,
            'problem_s': problem_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass
    
    def _get_soln_by_expr_type(self, expr, x, x0, x1):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        I_stmts = []
        if hasattr(expr, 'args'):
            sOut = f"""
            expr = {type(expr).__name__}{expr.args}
            """
            self._log(sOut)
            pass
        
        expr_cls_name = type(expr).__name__
        if expr_cls_name == 'exp':
            stmts, I_soln = self._get_soln_exp(expr, x, x0, x1)
            I_stmts.extend(stmts)
            return I_stmts, I_soln
            pass
        
        if expr_cls_name == 'sin':
            stmts, I_soln = self._get_soln_sin(expr, x, x0, x1)
            I_stmts.extend(stmts)
            return I_stmts, I_soln
            pass
        
        if expr_cls_name == 'cos':
            stmts, I_soln = self._get_soln_cos(expr, x, x0, x1)
            I_stmts.extend(stmts)
            return I_stmts, I_soln
            pass
        
        if expr_cls_name == 'cot':
            stmts, I_soln = self._get_soln_cot(expr, x, x0, x1)
            I_stmts.extend(stmts)
            return I_stmts, I_soln
            pass
        
        if expr_cls_name == 'tan':
            stmts, I_soln = self._get_soln_tan(expr, x, x0, x1)
            I_stmts.extend(stmts)
            return I_stmts, I_soln
            pass
        
        if expr_cls_name == 'sec':
            stmts, I_soln = self._get_soln_sec(expr, x, x0, x1)
            I_stmts.extend(stmts)
            return I_stmts, I_soln
            pass
        
        if expr_cls_name == 'csc':
            stmts, I_soln = self._get_soln_csc(expr, x, x0, x1)
            I_stmts.extend(stmts)
            return I_stmts, I_soln
            pass
        
        # dy_dx = Integral(expr, x).doit()
        dy_dx = self._doit_with_timeout(Integral(expr, x))
        dy_dx_cls_name = type(dy_dx).__name__
        soln_item = f"""
        dy_dx = {dy_dx}
        dy_dx_cls_name = {dy_dx_cls_name}
        """
        #I_stmts.append(soln_item.strip())
        
        dy_dx_rewritten = dy_dx.rewrite(sec)
        if type(dy_dx_rewritten).__name__ == 'sec':     
            stmts, I_soln = self._get_soln_by_D0(expr, x, x0, x1, dy_dx_rewritten)
            I_stmts.extend(stmts)
            return I_stmts, I_soln
            pass
        
        dy_dx_rewritten = dy_dx.rewrite(csc)
        if type(-dy_dx_rewritten).__name__ == 'csc':     
            stmts, I_soln = self._get_soln_by_D0(expr, x, x0, x1, dy_dx_rewritten)
            I_stmts.extend(stmts)
            return I_stmts, I_soln
            pass
        
        if expr_cls_name == 'Pow':
            stmts, I_soln = self._get_soln_Pow(expr, x, x0, x1)
            I_stmts.extend(stmts)
            return I_stmts, I_soln
            pass

        
        
        return None, None
        pass

    def _get_problem_s(self, expr, x, x0, x1):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        problem_s = f"""
        Find integration of $\\displaystyle {latex(expr)}
        $ with respect to $\\displaystyle {latex(x)}
        $
        """
        if not (x0 is None or x1 is None):
            problem_s = f"""{problem_s.strip()} in the range of $\\displaystyle 
            \\left({latex(x0)}, {latex(x1)} \\right).
            $"""
        else:
            problem_s = f"""{problem_s.strip()}."""
        return problem_s
    
    def _has_symbol(self, expr, symb):
        expr = sympify(expr)
        if not hasattr(expr, 'free_symbols'):
            return False
        if symb in expr.free_symbols:
            return True
        return False

    def _get_D0_Pow(self, expr, x):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        if not self._has_symbol(expr.args[0], x):
            D0 = Derivative(self.a ** self.x, self.x)
            return D0

        if expr.args[1] == -1:
            D0 = Derivative(log(self.x), self.x)
            return D0

        if not hasattr(expr.args[0], 'args'):
            D0 = Derivative(self.x ** self.n, self.x)
            return D0

        expr_cls_name = type(expr.args[0]).__name__
        if expr_cls_name == 'sec':
            D0 = Derivative(tan(self.x), self.x)
            return D0
        if expr_cls_name == 'csc':
            D0 = Derivative(cot(self.x), self.x)
            return D0

        D0 = Derivative(self.x ** self.n, self.x)
        return D0
        pass

    def _get_soln_Pow(self, expr, x, x0, x1):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        soln_items = []

        D0 = self._get_D0_Pow(expr, x)            
        
        D0_doit = D0.doit()
        D0_eq = Eq(D0, D0_doit)

        soln_item = f"""
        We know that:
        
        $\\displaystyle {self._modified_latex(D0_eq)}$
        
        Using the above relation:
        """
        soln_items.append(soln_item.strip())


        I_stmts, I_soln = self._Integral_doit(expr, x, x0, x1)
        soln_items.extend(I_stmts)
        
        
        return soln_items, I_soln
        pass
    
    def _get_soln_exp(self, expr, x, x0, x1):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        soln_items = []

        D0 = Derivative(exp(self.x), self.x)
        D0_doit = D0.doit()
        D0_eq = Eq(D0, D0_doit)

        soln_item = f"""
        We know that:
        
        $\\displaystyle {self._modified_latex(D0_eq)}$
        
        Using the above relation:
        """
        soln_items.append(soln_item.strip())

        I_stmts, I_soln = self._Integral_doit(expr, x, x0, x1)
        soln_items.extend(I_stmts)
        
        
        return soln_items, I_soln
        pass
    
    def _get_soln_sin(self, expr, x, x0, x1):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        soln_items = []

        D0 = Derivative(cos(self.x), self.x)
        D0_doit = D0.doit()
        D0_eq = Eq(D0, D0_doit)

        soln_item = f"""
        We know that:
        
        $\\displaystyle {self._modified_latex(D0_eq)}$
        
        Using the above relation:
        """
        soln_items.append(soln_item.strip())

        I_stmts, I_soln = self._Integral_doit(expr, x, x0, x1)
        soln_items.extend(I_stmts)
        
        
        return soln_items, I_soln
        pass
    
    def _get_soln_cos(self, expr, x, x0, x1):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        soln_items = []

        D0 = Derivative(sin(self.x), self.x)
        D0_doit = D0.doit()
        D0_eq = Eq(D0, D0_doit)

        soln_item = f"""
        We know that:
        
        $\\displaystyle {self._modified_latex(D0_eq)}$
        
        Using the above relation:
        """
        soln_items.append(soln_item.strip())

        I_stmts, I_soln = self._Integral_doit(expr, x, x0, x1)
        soln_items.extend(I_stmts)
        
        
        return soln_items, I_soln
        pass
    
    def _get_soln_cot(self, expr, x, x0, x1):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        soln_items = []

        D0 = Derivative(log(sin(self.x)), self.x)
        D0_doit = D0.doit()
        D0_eq = Eq(D0, D0_doit)

        soln_item = f"""
        We know that:
        
        $\\displaystyle {self._modified_latex(D0_eq)}$
        
        Using the above relation:
        """
        soln_items.append(soln_item.strip())

        I_stmts, I_soln = self._Integral_doit(expr, x, x0, x1)
        soln_items.extend(I_stmts)
        
        
        return soln_items, I_soln
        pass
    
    def _get_soln_tan(self, expr, x, x0, x1):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        soln_items = []

        D0 = Derivative(log(cos(self.x)), self.x)
        D0_doit = D0.doit()
        D0_eq = Eq(D0, D0_doit)

        soln_item = f"""
        We know that:
        
        $\\displaystyle {self._modified_latex(D0_eq)}$
        
        Using the above relation:
        """
        soln_items.append(soln_item.strip())

        I_stmts, I_soln = self._Integral_doit(expr, x, x0, x1)
        soln_items.extend(I_stmts)
        
        
        return soln_items, I_soln
        pass
    
    def _get_soln_sec(self, expr, x, x0, x1):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        soln_items = []

        D0 = Derivative(log(sec(self.x) + tan(self.x)), self.x)
        D0_doit = D0.doit()
        D0_eq = Eq(D0, D0_doit)

        soln_item = f"""
        We know that:
        
        $\\displaystyle {self._modified_latex(D0_eq)}$
        
        Using the above relation:
        """
        soln_items.append(soln_item.strip())

        I_stmts, I_soln = self._Integral_doit(expr, x, x0, x1)
        soln_items.extend(I_stmts)
        
        
        return soln_items, I_soln
        pass
    
    def _get_soln_csc(self, expr, x, x0, x1):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        soln_items = []

        D0 = Derivative(log(csc(self.x) - cot(self.x)), self.x)
        D0_doit = D0.doit()
        D0_eq = Eq(D0, D0_doit)

        soln_item = f"""
        We know that:
        
        $\\displaystyle {self._modified_latex(D0_eq)}$
        
        Using the above relation:
        """
        soln_items.append(soln_item.strip())

        I_stmts, I_soln = self._Integral_doit(expr, x, x0, x1)
        soln_items.extend(I_stmts)
        
        
        return soln_items, I_soln
        pass
    
    def _get_soln_by_D0(self, expr, x, x0, x1, D0):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        soln_items = []

        D0 = Derivative(D0, self.x)
        D0_doit = D0.doit()
        D0_eq = Eq(D0, D0_doit)

        soln_item = f"""
        We know that:
        
        $\\displaystyle {self._modified_latex(D0_eq)}$
        
        Using the above relation:
        """
        soln_items.append(soln_item.strip())

        I_stmts, I_soln = self._Integral_doit(expr, x, x0, x1)
        soln_items.extend(I_stmts)
        
        
        return soln_items, I_soln
        pass
    
    def _Integral_doit(self, expr, x, x0, x1):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        I0 = self._Integral(expr, x, x0, x1)
        I0_no_range = Integral(expr, x)
        
        I0_no_range_doit = self._doit_with_timeout(I0_no_range)
        I0_doit = self._doit_with_timeout(I0)

        
        integral_const = Symbol('C')
        
        I_stmts = []
        I_stmts.append(f"$\\displaystyle {latex(I0)} $")

        if (I0_no_range_doit is None or I0_doit is None):
            I_soln = None
            I_stmts.append(f"Failed to compute the above expression.")
            return I_stmts, I_soln
            pass
        
        if (x0 is None or x1 is None):
            I_soln = ueval(I0_no_range_doit) + ueval(integral_const)
            I1 = Symbol(f""" 
                {latex(I_soln)}
                \\qquad
                \\text{{where }} {latex(integral_const)}
                \\text{{ is an integration constant. }}
                """.strip())  
            
            I_stmts.append(f"$\\displaystyle = {latex(I1)} $")
            
            return I_stmts, I_soln
        
        I1 = Symbol(f""" 
                \\left[ {latex(I0_no_range_doit)} \\right]_{{{latex(x0)}}}^{{{latex(x1)}}}
                """.strip())
        
        I2_1 = I0_no_range_doit.subs(x, ueval(x0))
        I2_2 = I0_no_range_doit.subs(x, ueval(x1))
        I2 = Symbol(f""" 
                \\left[ {latex(I2_2)} \\right]
                - \\left[ {latex(I2_1)} \\right]
                """.strip())
        
        I_soln = simplify(I0_doit)
        
        I_stmts.append(f"$\\displaystyle = {latex(I1)} $")
        I_stmts.append(f"$\\displaystyle = {latex(I2)} $")
        
        if str(I0_doit) != str(I_soln):
            I_stmts.append(f"$\\displaystyle = {latex(I0_doit)} $")
        
        I_stmts.append(f"$\\displaystyle = {latex(I_soln)} $")
        
        return I_stmts, I_soln
        pass

    def _doit_with_timeout(self, intgr_expr):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        
        method = intgr_expr.doit
        timeout = 5
        result = run_with_timeout(
                          f'Thread_intgration_doit', 
                          method, 
                          (), 
                          timeout = timeout, 
                          verbose = self.verbose)

        if not result:
            sOut = f""" 
            Failed to compute integration in {timeout} seconds 
            intgr_expr = {intgr_expr}
            """
            self._log(sOut)
            #sOut = f""" 
            #intgr_expr_simplify = {intgr_expr.simplify(force = True)}
            #"""
            #self._log(sOut)
            return None
            
        return result
        pass

    def _Integral(self, expr, x, x0, x1):
        if (x0 is None or x1 is None):
            return Integral(expr, x)
        return Integral(expr, (x, x0, x1))
        pass
    
    def transform_ax_plus_b(self, expr, sexpr, x):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        u_symb = Symbol('u') #(x)
        u = Function('u')(x)

        d_x_dx = Derivative(x, x)
        d_x_dx_doit = d_x_dx.doit()
        
        xu = solve(Eq(u_symb, sexpr), x)[-1]
        xu = xu.subs(u_symb, u)
        
        d_xu_du = Derivative(xu, u)
        d_xu_du_doit = d_xu_du.doit()
        

        i_expr_dx = Integral(expr, x)
        i_expr_dx_doit = self._doit_with_timeout(i_expr_dx)
        i_expr_dx_doit = expand(i_expr_dx_doit)
        
        
        mexpr_ueval = expr * d_xu_du_doit
        mexpr_ueval = mexpr_ueval.subs(sexpr, u_symb, evaluate = False)

        i_mexpr_ueval_dx = Integral(mexpr_ueval, u)

        mexpr_ueval_2 = mexpr_ueval.subs(x, ueval(xu), evaluate = False)
        
        i_mexpr_ueval_2_dx = Integral(mexpr_ueval_2, u)

        mexpr_ueval_3 = simplify(mexpr_ueval_2)
        
        i_mexpr_ueval_3_dx = Integral(mexpr_ueval_3, u)

        mexpr_ueval_4 = expand(mexpr_ueval_3.subs(u_symb, u))
        
        i_mexpr_ueval_4_dx = Integral(mexpr_ueval_4, u)

        mexpr = expand(simplify(mexpr_ueval_4))
        
        i_mexpr_dx = Integral(mexpr, u)
        i_mexpr_dx_doit = self._doit_with_timeout(i_mexpr_dx)
        i_mexpr_dx_doit = expand(i_mexpr_dx_doit)
        
        i_mexpr_dx_doit_subs = i_mexpr_dx_doit.subs(u, sexpr)
        
        soln_items = []
        
        soln_item = f"""
        Let:
        
        $\\displaystyle {latex(u)} = {latex(sexpr)} $
        
        $\\displaystyle \\implies {latex(x)} = {latex(xu)} $
        
        $\\displaystyle \\implies d{latex(x)} = {latex(d_xu_du)} \\; d{latex(u)} $
        
        $\\displaystyle \\implies d{latex(x)} = {latex(d_xu_du_doit)} \\; d{latex(u)} $
        """
        soln_items.append(soln_item.strip())

        soln_item = f"""
        Now:
        
        $\\displaystyle {latex(i_expr_dx)} $
        
        $\\displaystyle = {latex(i_mexpr_ueval_dx)} $
        
        $\\displaystyle = {latex(i_mexpr_ueval_2_dx)} $
        
        $\\displaystyle = {latex(i_mexpr_ueval_3_dx)} $
        
        $\\displaystyle = {latex(i_mexpr_ueval_4_dx)} $
        
        """
        soln_items.append(soln_item.strip())
        
        soln_item = f"""
        Last Steps:
        
        $\\displaystyle = {latex(i_mexpr_dx)} $
        
        $\\displaystyle = {latex(i_mexpr_dx_doit)} $
        
        $\\displaystyle = {latex(i_mexpr_dx_doit_subs)} $
        
        $\\displaystyle = {latex(simplify(i_mexpr_dx_doit_subs))} $
        
        """
        soln_items.append(soln_item.strip())
        
        soln_item = f"""
        Hence:
        
        $\\displaystyle {latex(i_expr_dx)} = {latex(i_expr_dx_doit)} $
        
        """
        soln_items.append(soln_item.strip())
        
        soln_s = "\n\n".join(soln_items)

        result = {
            'x': x,
            'd_x_dx': d_x_dx,
            'd_x_dx_doit': d_x_dx_doit,
            
            'u': u,
            'xu': xu,

            'd_xu_du': d_xu_du,
            'd_xu_du_doit': d_xu_du_doit,

            'expr': expr,
            'i_expr_dx': i_expr_dx,
            'i_expr_dx_doit': i_expr_dx_doit,

            'mexpr_ueval': mexpr_ueval,
            'mexpr': mexpr,
            'i_mexpr_ueval_dx': i_mexpr_ueval_dx,
            'i_mexpr_dx': i_mexpr_dx,
            'i_mexpr_dx_doit': i_mexpr_dx_doit,

            'soln_s': soln_s,
            }
        
        kv_str = []
        for k,v in result.items():
            if k == 'soln_s':
                continue
            kv_str.append(f"{k}: $\\displaystyle {latex(v)}$")
            pass
        result['kv_str'] = "\n\n".join(kv_str)
        
        return result
        pass

    def _get_mod_numr_terms(self, expr_terms):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        numrs = []
        denrs = []
        for expr_term in expr_terms:
            if not "/" in f"{expr_term}":
                numr = expr_term
                denr = sympify(1)
            else:
                numr, denr = f"{expr_term}".split("/")
                numr = sympify(numr)
                denr = sympify(denr)
            numrs.append(numr)
            denrs.append(denr)
            pass
        
        common_denr_terms = [denr for denr in denrs]
        
        mod_numrs = []
        for idx, numr in enumerate(numrs):
            denr_terms = [term for jdx,term in enumerate(common_denr_terms)
                         if jdx != idx]
            
            if numr == 1:
                mod_numr = ueval(Mul(*denr_terms))
            else:
                mod_numr = ueval(numr) * ueval(Mul(*denr_terms))
                
            mod_numrs.append(mod_numr)
            pass  
        
        return mod_numrs  
        pass
        
    def _get_common_denr_symbs(self, expr_terms):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        numrs = []
        denrs = []
        
        print(f"expr_terms = {expr_terms}")
        
        for expr_term in expr_terms:
            if not "/" in f"{expr_term}":
                numr = expr_term
                denr = sympify(1)
            else:
                numr, denr = f"{expr_term}".split("/")
                numr = sympify(numr)
                denr = sympify(denr)
            numrs.append(numr)
            denrs.append(denr)
            pass
        
        common_denr_terms = [self._convert_term_to_symb(denr) for denr in denrs]
        common_denr = Mul(*common_denr_terms)
        
        mod_numr_terms = []
        for idx, numr in enumerate(numrs):
            denr_terms = [self._convert_term_to_symb(term) 
                          for jdx,term in enumerate(common_denr_terms)
                         if jdx != idx]
            
            if numr == 1:
                mod_numr = Mul(*denr_terms)
            else:
                mod_numr = self._convert_term_to_symb(numr) * Mul(*denr_terms)
                
            mod_numr_terms.append(mod_numr)
            pass  
        
        return mod_numr_terms, common_denr  
        pass
    
    def _convert_term_to_symb(self, term):
        ops = ['+', '-', '*', '/', 'log']
        ops = ['+', '-', '/', 'log']
        
        if isinstance(term, Add):
            return Symbol(f"\\left({latex(term)}\\right)")
        
        for op in ops:
            if f"{term}".startswith(op):
                return Symbol(f"\\left({latex(term)}\\right)")
            pass
        return Symbol(f"{latex(term)}")
        pass
    
    def transform_numerator(self, expr, x):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        soln_items = []
        
        print(f"expr.args = {expr.args} ")
        
        t1, t2 = expr.args
        
        i_expr_dx = Integral(expr, x)
        i_expr_dx_doit = self._doit_with_timeout(i_expr_dx)
        i_expr_dx_doit = expand(i_expr_dx_doit)
        
        i_expr = i_expr_dx_doit
        
        d_i_expr_dx_doit = Derivative(i_expr, x).doit()
        d_i_expr_dx_doit_expanded = expand(d_i_expr_dx_doit)
        
        
        soln_item = f"""
        $\\displaystyle {latex(i_expr_dx)} $
        """
        soln_items.append(soln_item.strip())

        if isinstance(d_i_expr_dx_doit_expanded, Add):
            
            cls_name = type(d_i_expr_dx_doit_expanded)
    
            mod_terms = [factor(term) for term in expand(d_i_expr_dx_doit_expanded).args]
            #mod_expr = type(d_i_expr_dx_doit)(*mod_terms)


            I3_terms = []
            I3_doit_terms = []
            for mod_term in mod_terms:
                mod_term_cls = type(mod_term)
                
                print(f"mod_term_cls = {mod_term_cls}")
                if not hasattr(mod_term, 'arg'):
                    I3_terms.append(mod_term)
                    I3_doit_terms.append(mod_term)
                    continue
                    
                non_var_terms = []
                var_terms = []
                for arg in mod_term.args:
                    if x in arg.free_symbols:
                        var_terms.append(arg)
                    else:
                        non_var_terms.append(arg)
                    pass
                
                if len(non_var_terms) != 0:
                    non_var_terms = mod_term_cls(*non_var_terms)
                else:
                    non_var_terms = sympify(0)
                    
                if len(var_terms) != 0:
                    var_terms = mod_term_cls(*var_terms)
                else:
                    var_terms = sympify(0)
                    
                var_terms_doit = self._doit_with_timeout(Integral(var_terms, x))
                var_terms_doit = simplify(var_terms_doit)
                
                term = mod_term_cls(ueval(non_var_terms), ueval(var_terms))
                doit_term = mod_term_cls(ueval(non_var_terms), ueval(var_terms_doit))
                
                I3_terms.append(ueval(term))
                I3_doit_terms.append(ueval(doit_term))
                
                pass


            mod_numr_symbs, common_denr = self._get_common_denr_symbs(mod_terms)
            
            mod_numr = cls_name(*mod_numr_symbs)
            Imod0_1 = Integral( mod_numr / common_denr, x)
            
            mod_numr_terms = self._get_mod_numr_terms(mod_terms)

            mod_numr_2_symb = [self._convert_term_to_symb(simplify(term))  
                               for term in mod_numr_terms]

            Imod0_2 = Integral( cls_name(*mod_numr_2_symb) / common_denr, x)


            mod_numr_2_simp = [ueval(simplify(term))  for term in mod_numr_terms]
            mod_factor = factor(expand(cls_name(*mod_numr_2_simp)))
            Imod0_3 = Integral( mod_factor / common_denr, x)

            numr_simp = simplify(cls_name(*mod_numr_terms))
            Imod0_4 = Integral( numr_simp / common_denr, x)



            mod_terms = [ueval(mod_term) for mod_term in mod_terms]
            Imod0 = Integral( cls_name(*mod_terms), x)
            
            
            Imod1 = Integral( cls_name(*I3_terms), x)
            
            Imod2 = cls_name(*I3_doit_terms)
            
            Imod3_terms = [ueval(simplify(term)) for term in I3_doit_terms]
            Imod3 = cls_name(*Imod3_terms)
            
            soln_item = f"""
            
            Imod0_4 $\\displaystyle = {latex(Imod0_4, mul_symbol = 'dot')} $
            
            Imod0_2 $\\displaystyle = {latex(Imod0_2, mul_symbol = 'dot')} $
            
            Imod0_1 $\\displaystyle = {latex(Imod0_1, mul_symbol = 'dot')} $
            
            Imod0 $\\displaystyle = {latex(Imod0)} $
            
            Imod1 $\\displaystyle = {latex(Imod1)} $
            
            Imod2 $\\displaystyle = {latex(Imod2, mul_symbol = 'dot')} $
            
            Imod3 $\\displaystyle = {latex(Imod3)} $
            
            """
            soln_items.append(soln_item.strip())
            pass
        
        

        soln_item = f"""
        Hnece: 
        $\\displaystyle {latex(i_expr_dx)} $
        """
        soln_items.append(soln_item.strip())
        
        
        soln_item = f"""
        i_expr $\\displaystyle = {latex(i_expr)} $
        
        """
        soln_items.append(soln_item.strip())
        
        soln_s = "\n\n".join(soln_items)

        result = {
            'x': x,
            'expr': expr,
            'i_expr': i_expr,
            'i_expr_dx': i_expr_dx,
            'i_expr_dx_doit': i_expr_dx_doit,
            
            'soln_s': soln_s,
            }
        
        kv_str = []
        for k,v in result.items():
            if k == 'soln_s':
                continue
            kv_str.append(f"{k}: $\\displaystyle {latex(v)}$")
            pass
        result['kv_str'] = "\n\n".join(kv_str)
        
        return result
        pass
    
    def _get_args_factors(self, expr, x, y):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        expr_factors = factor(expr)

        y_symb = Symbol('y')
        
        if not isinstance(expr_factors, Mul):
            expr_subs = expr_factors.subs(y, y_symb)
            fcoeff = 1
            fx = 1
            fy = 1
            fxy = 1

            if (y_symb in expr_subs.free_symbols and
                x in expr_subs.free_symbols):
                fxy = expr_subs
                return fxy, fy, fx, fcoeff
                
            if y_symb in expr_subs.free_symbols:
                fy = expr_subs
                return fxy, fy, fx, fcoeff

            if x in expr_subs.free_symbols:
                fx = expr_subs
                return fxy, fy, fx, fcoeff

            fcoeff = expr_subs
            return fxy, fy, fx, fcoeff
            pass
        
        fx_args = [1]
        fy_args = [1]
        fxy_args = [1]
        fcoeff_args = [1]
        for arg in expr_factors.args:
            arg_subs = arg.subs(y, y_symb)
            
            if (y_symb in arg_subs.free_symbols and
                x in arg_subs.free_symbols):
                fxy_args.append(arg)
                continue
                
            if y_symb in arg_subs.free_symbols:
                fy_args.append(arg)
                continue

            if x in arg_subs.free_symbols:
                fx_args.append(arg)
                continue
            
            fcoeff_args.append(arg)
            pass
        fcoeff = Mul(*fcoeff_args)
        fx = Mul(*fx_args)
        fy = Mul(*fy_args)
        fxy = Mul(*fxy_args)
        return fxy, fy, fx, fcoeff
        pass

    def get_default_symbs(self, var_names):
        default_symbs = []
        for var_name in var_names:
            if hasattr(self, var_name):
                default_symbs.append(getattr(self, var_name))
            else:
                default_symbs.append(Symbol(var_name))
                pass
            pass
        return default_symbs
        pass

    def _modified_latex(self, expr, funct_symbs = None, mul_symbol = None):
        expr = self._convert_function_to_symbol(expr, funct_symbs = funct_symbs)
        expr_latex = latex(expr, mul_symbol = mul_symbol)
        
        expr_latex = expr_latex.replace("\\partial", "d")
        
        return expr_latex
        pass

    def _convert_function_to_symbol(self, expr, funct_symbs = None):
        if funct_symbs is None:
            funct_symbs = [self.y]
        
        temp_dict = {}
        for funct_symb in funct_symbs:
            temp_dict[funct_symb] = Symbol(funct_symb.name)
            pass
        
        if not temp_dict:
            return expr

        expr_subs = expr.subs(temp_dict)
        
        return expr_subs
        pass

    pass
#class IntegrationHelper:
