'''
Created on 16-Apr-2021

@author: bthakur
'''
import random
import inspect
import traceback


from xv.km.util import latex_fraction

from sympy import (symbols, Symbol, limit, latex, Function,
                   Limit, Derivative, simplify,
                   expand_log,
                   Mul, Add, Pow, UnevaluatedExpr as ueval,
                   Sum, binomial, factorial, Rational
                   )
import sympy as sp
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 xv.km.util._km_template_util import KmTemplateUtil
       

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


      
class DifferentiationManager(BaseManager):
    '''
    Differentiation 
    Example:
        ke = DifferentiationManager()
        ke
        
        ke.printProblemTypes()
        
        ke.getRandomProblem()
        ke.getRandomProblem(problem_type = 0)
        ke.getRandomProblem(problem_type = 1)
        ...
        
        ke.printProblem()
        ke.printAnswer()
        ke.printSolution()
        
        
    '''    
    
    
    def __new__(cls, *args, **kwargs):
        instance = super(DifferentiationManager, cls).__new__(cls, *args, **kwargs)
        for base_class in DifferentiationManager.__bases__:
            base_class.__init__(instance, *args, **kwargs)
        return instance
    
    def __init__(self, *args, **kwargs):
        self.base_helper = BaseHelper(**kwargs)

        pass
    

    def _init_problem_types(self, *args, **kwargs):
        self._append_problem_types([
                '_problem_delta_of_a_function',
                '_problem_define_limit',
                '_problem_define_continuity',
                '_problem_define_derivative',
                '_problem_derivative_rule_add',
                '_problem_derivative_rule_mul',
                '_problem_compute_derivative_x_power_n',
                '_problem_derivative_finite_difference',
                '_problem_derivative_of_standard_functions',
                '_problem_derivative_sin_cos',
                '_problem_derivative_trig_product_rule',
                '_problem_derivative_product_rule',
                '_problem_derivative_exponent_x_rule',
                '_problem_derivative_p_q',
                '_problem_derivative_custom_function',
                '_problem_intutive_derivative_computation',
                '_problem_derivative_difficult',
                '_problem_bivariate_derivative_computation',
                '_problem_derivative_advanced_questions',
                '_problem_stirlings_approximation',
                '_problem_WORKING',
             ])
        pass


    def _problem_delta_of_a_function(self, *args, **kwargs):
        dt_helper = self.base_helper.get_calculus_helper().get_differentiation_theory_helper()

        x = dt_helper.x
        exprs = dt_helper.get_standard_exprs(x = x)
        f = random.choice(exprs)
        
        h = dt_helper.h
        x0 = x
        x1 = x0 + h
        
        
        result = dt_helper.get_function_delta(f, x, x0, x1)
        
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        
        problem_s = f"""
        Find the change in $\\displaystyle {latex(f)} \\text{{ when }} {latex(x)}
        \\text{{ changes from }} {latex(x0)} \\text{{ to }}  {latex(x1)} .$
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        The change in $\\displaystyle {latex(f)} \\text{{ when }} {latex(x)}
        \\text{{ changes from }} {latex(x0)} \\text{{ to }}  {latex(x1)} \\text{{ is }}
        $
        
        {result['soln_s']}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {result['answer_s']}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    def _problem_define_limit(self, *args, **kwargs):
        dt_helper = self.base_helper.get_calculus_helper().get_differentiation_theory_helper()
        
        x = dt_helper.x
        exprs = dt_helper.get_standard_exprs(x = x)
        f = random.choice(exprs)
        
        x0 = Symbol(rf"{latex(x)}_{{0}}")
        result = dt_helper.define_limit(f, x, x0)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        
        problem_s = f"""
        Define limit of $\\displaystyle {latex(f)} \\text{{ at }} {latex(x0)}$.
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        Assuming that $\\displaystyle 
        {latex(f)} \\text{{ exists at }} {latex(x0)}
        $, limit of $\\displaystyle {latex(f)} \\text{{ at }} {latex(x)}$ is
        
        {result['soln_s']}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {result['answer_s']}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    def _problem_define_continuity(self, *args, **kwargs):
        dt_helper = self.base_helper.get_calculus_helper().get_differentiation_theory_helper()
        
        x = dt_helper.x
        exprs = dt_helper.get_standard_exprs(x = x)
        f = random.choice(exprs)

        x0 = Symbol(rf"{latex(x)}_{{0}}")
        result = dt_helper.define_continuity(f, x, x0)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        
        problem_s = f"""
        Define continuity of $\\displaystyle {latex(f)} \\text{{ at }} {latex(x0)}$.
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        Assuming that limit of $\\displaystyle 
        {latex(f)} \\text{{ exists at }} {latex(x0)}
        $, $\\displaystyle {latex(f)} \\text{{ is continuous at }} {latex(x0)}
        $ if and only if  
        
        {result['soln_s']}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {result['answer_s']}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    
    def _problem_define_derivative(self, *args, **kwargs):
        dt_helper = self.base_helper.get_calculus_helper().get_differentiation_theory_helper()
        
        x = dt_helper.x
        exprs = dt_helper.get_standard_exprs(x = x)
        f = random.choice(exprs)

        result = dt_helper.define_derivative(f, x)
        xh = result['xh']
        
        df_dx_latex = result['df_dx_latex']
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        
        problem_s = f"""
        Define derivative of $\\displaystyle 
        {latex(f)} \\text{{ with respect to }} {latex(x)}$.
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        Assuming that limit of $\\displaystyle 
        {latex(f)} \\text{{ exists at }} {latex(x)} \\text{{ and }} {latex(xh)}
        $, derivative of $\\displaystyle {latex(f)} \\text{{ with respect to }} {latex(x)}$
        
        {result['soln_s']}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {result['answer_s']}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    
    def _problem_derivative_rule_add(self, *args, **kwargs):
        dt_helper = self.base_helper.get_calculus_helper().get_differentiation_theory_helper()
        
        #f = dt_helper.f
        #g = dt_helper.g
        
        x = dt_helper.x
        exprs = dt_helper.get_standard_exprs(x = x)
        f, g = random.sample(exprs, 2)

        
        x = dt_helper.x
        h = dt_helper.h
        result = dt_helper.get_derivative_rule_add(f, g, x, h)
        
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        
        problem_s = f"""
        Define derivative of $\\displaystyle 
        {latex(f)} + {latex(g)}  \\text{{ with respect to }} {latex(x)}$.
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        Derivative of $\\displaystyle 
        {latex(f)} + {latex(g)}  \\text{{ with respect to }} {latex(x)}$
        
        {result['soln_s']}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {result['answer_s']}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    
    def _problem_derivative_rule_mul(self, *args, **kwargs):
        dt_helper = self.base_helper.get_calculus_helper().get_differentiation_theory_helper()
        
        #f = dt_helper.f
        #g = dt_helper.g
        
        x = dt_helper.x
        exprs = dt_helper.get_standard_exprs(x = x)
        f, g = random.sample(exprs, 2)

        x = dt_helper.x
        h = dt_helper.h
        result = dt_helper.get_derivative_rule_mul(f, g, x, h)
        
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        
        problem_s = f"""
        Define derivative of $\\displaystyle 
        {latex(f)} + {latex(g)}  \\text{{ with respect to }} {latex(x)}$.
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        Derivative of $\\displaystyle 
        {latex(f)} + {latex(g)}  \\text{{ with respect to }} {latex(x)}$
        
        {result['soln_s']}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {result['answer_s']}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    
    def _problem_compute_derivative_x_power_n(self, *args, **kwargs):
        dt_helper = self.base_helper.get_calculus_helper().get_differentiation_theory_helper()

        result = dt_helper.compute_derivative_x_power_n()
        f = result['f']
        x = result['x']
        
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        
        problem_s = f"""
        Find derivative of $\\displaystyle 
        {latex(f)} \\text{{ with respect to }} {latex(x)}$.
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        Derivative of $\\displaystyle {latex(f)} \\text{{ with respect to }} {latex(x)}$
        
        {result['soln_s']}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        
        answer_s = f"""
        {result['answer_s']}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    
    def _problem_derivative_finite_difference(self, *args, **kwargs):
        adc_helper = self.base_helper.get_calculus_helper().get_auto_derivation_calc_helper()

        f = kwargs.get('f', None)
        x = kwargs.get('x', None)
        x0 = kwargs.get('x0', None)

        if (f is None or 
            x is None or  
            x0 is None):
            f, x, x0 = adc_helper.get_random_function(category = "finite_difference")
            pass
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        result = adc_helper.get_solution(f, x, x0)
        result_str = adc_helper.solution_to_str(result)
        #print(result)
        
        problem_s = f"""
        Find derivative of the $
        \\displaystyle  {latex(f)}$  with respect to {x}:
         
        $\\displaystyle {result['Derivative_latex']} $
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        {result_str}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        $\\displaystyle {result['Derivative_latex']} 
        
        = {latex(result['answer'])} $
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass


    def _problem_derivative_of_standard_functions(self, *args, **kwargs):
        sd_helper = self.base_helper.get_calculus_helper().get_std_differentiation_helper()

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        result = sd_helper.get_standrad_derivative()
        
        prob_s = f"""
        {result['prob_s']}
        """
        
        problemTemplate.add_problem(prob_s.strip(), 'text/html')


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

    def _problem_derivative_sin_cos(self, *args, **kwargs):
        adc_helper = self.base_helper.get_calculus_helper().get_auto_derivation_calc_helper()

        f = kwargs.get('f', None)
        x = kwargs.get('x', None)
        x0 = kwargs.get('x0', None)

        if (f is None or 
            x is None or  
            x0 is None):
            f, x, x0 = adc_helper.get_random_function(category = 'sin_cos')
            pass
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        adc_helper = self.base_helper.get_calculus_helper().get_auto_derivation_calc_helper()
        result = adc_helper.get_solution(f, x, x0)
        result_str = adc_helper.solution_to_str(result)
        #print(result)
        
        problem_s = f"""
        Find derivative of the $
        \\displaystyle  {latex(f)}$  with respect to {x}:
         
        $\\displaystyle {latex(result['Derivative'])} $
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        {result_str}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        $\\displaystyle {latex(result['Derivative'])} 
        
        = {latex(result['answer'])} $
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

        
    def _problem_derivative_trig_product_rule(self, *args, **kwargs):
        adc_helper = self.base_helper.get_calculus_helper().get_auto_derivation_calc_helper()

        f = kwargs.get('f', None)
        x = kwargs.get('x', None)
        x0 = kwargs.get('x0', None)

        if (f is None or 
            x is None or  
            x0 is None):
            f, x, x0 = adc_helper.get_random_function(category = 'trig_product')
            pass
        
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        adc_helper = self.base_helper.get_calculus_helper().get_auto_derivation_calc_helper()
        result = adc_helper.get_solution(f, x, x0)
        result_str = adc_helper.solution_to_str(result)
        #print(result)
        
        problem_s = f"""
        Find derivative of the $
        \\displaystyle  {latex(f)}$  with respect to {x}:
         
        $\\displaystyle {latex(result['Derivative'])} $
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        {result_str}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        $\\displaystyle {latex(result['Derivative'])} 
        
        = {latex(result['answer'])} $
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass
        
    def _problem_derivative_product_rule(self, *args, **kwargs):
        adc_helper = self.base_helper.get_calculus_helper().get_auto_derivation_calc_helper()

        f = kwargs.get('f', None)
        x = kwargs.get('x', None)
        x0 = kwargs.get('x0', None)

        if (f is None or 
            x is None or  
            x0 is None):
            f, x, x0 = adc_helper.get_random_function(category = 'product')
            pass
        
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        adc_helper = self.base_helper.get_calculus_helper().get_auto_derivation_calc_helper()
        result = adc_helper.get_solution(f, x, x0)
        result_str = adc_helper.solution_to_str(result)
        #print(result)
        
        problem_s = f"""
        Find derivative of the $
        \\displaystyle  {latex(f)}$  with respect to {x}:
         
        $\\displaystyle {latex(result['Derivative'])} $
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        {result_str}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        $\\displaystyle {latex(result['Derivative'])} 
        
        = {latex(result['answer'])} $
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass
        
    def _problem_derivative_exponent_x_rule(self, *args, **kwargs):
        adc_helper = self.base_helper.get_calculus_helper().get_auto_derivation_calc_helper()

        f = kwargs.get('f', None)
        x = kwargs.get('x', None)
        x0 = kwargs.get('x0', None)

        if (f is None or 
            x is None or  
            x0 is None):
            f, x, x0 = adc_helper.get_random_function(category = 'exponent_x')
            pass
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        adc_helper = self.base_helper.get_calculus_helper().get_auto_derivation_calc_helper()
        result = adc_helper.get_solution(f, x, x0)
        result_str = adc_helper.solution_to_str(result)
        #print(result)
        
        problem_s = f"""
        Find derivative of the $
        \\displaystyle  {latex(f)}$  with respect to {x}:
         
        $\\displaystyle {latex(result['Derivative'])} $
        """
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')


        soln_s = f"""
        {result_str}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        $\\displaystyle {latex(result['Derivative'])} 
        
        = {latex(result['answer'])} $
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass
        
    def _fOne(self, x):
        return 1
    
    def _problem_derivative_p_q(self, *args, **kwargs):
        func_p_q = random.choice([
            (sp.sin, sp.cos, sp.tan),
            (self._fOne, sp.sin, sp.csc),
            (self._fOne, sp.cos, sp.sec),
            (sp.cos, sp.sin, sp.cot),
            ])
        pass



    def _problem_differentiation_overview_manager(self, *args, **kwargs):
        """
        @generate_notebook: Default True

        doc_style: xv_doc
        """

        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: 
        creating notebook and getting problem from DifferentiationOverviewManager
        """
        self._log(sInfo.strip())
        
        
        #create notebook
        generate_notebook = kwargs.get('generate_notebook', True)
        
        if generate_notebook:
            saveToFolder = f"differentiation"
            KmTemplateUtil.CREATE_MANAGER_NOTEBOOK("xv.math.calculus", 
                                                   "DifferentiationOverviewManager", 
                                                   saveToFolder = saveToFolder,
                                                   overwrite = False, 
                                                   verbose = self.verbose)

        #get a problem        
        from xv.math.calculus import DifferentiationOverviewManager
        kwargs['manager_class'] = DifferentiationOverviewManager
        kwargs['problem_type'] = None
        kwargs['generate_notebook'] = False
        problemTemplate = self._get_problem_from_a_manager(*args, **kwargs)
        return problemTemplate

        pass



    def _problem_limit_manager(self, *args, **kwargs):
        """
        @generate_notebook: Default True

        doc_style: xv_doc
        """

        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: 
        creating notebook and getting problem from LimitManager
        """
        self._log(sInfo.strip())
        
        
        #create notebook
        generate_notebook = kwargs.get('generate_notebook', True)
        
        if generate_notebook:
            saveToFolder = f"differentiation"
            KmTemplateUtil.CREATE_MANAGER_NOTEBOOK("xv.math.calculus", 
                                                   "LimitManager", 
                                                   saveToFolder = saveToFolder,
                                                   overwrite = False, 
                                                   verbose = self.verbose)

        #get a problem        
        from xv.math.calculus import LimitManager
        kwargs['manager_class'] = LimitManager
        kwargs['problem_type'] = None
        kwargs['generate_notebook'] = False
        problemTemplate = self._get_problem_from_a_manager(*args, **kwargs)
        return problemTemplate

        pass




    def _problem_xn_manager(self, *args, **kwargs):
        """
        @generate_notebook: Default True

        doc_style: xv_doc
        """

        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: 
        creating notebook and getting problem from XnManager
        """
        self._log(sInfo.strip())
        
        
        #create notebook
        generate_notebook = kwargs.get('generate_notebook', True)
        
        if generate_notebook:
            saveToFolder = f"differentiation"
            KmTemplateUtil.CREATE_MANAGER_NOTEBOOK("xv.math.calculus", 
                                                   "XnManager", 
                                                   saveToFolder = saveToFolder,
                                                   overwrite = False, 
                                                   verbose = self.verbose)

        #get a problem        
        from xv.math.calculus import XnManager
        kwargs['manager_class'] = XnManager
        kwargs['problem_type'] = None
        kwargs['generate_notebook'] = False
        problemTemplate = self._get_problem_from_a_manager(*args, **kwargs)
        return problemTemplate

        pass



    def _problem_differentiation_basics_manager(self, *args, **kwargs):
        """
        @generate_notebook: Default True

        doc_style: xv_doc
        """

        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: 
        creating notebook and getting problem from DifferentiationBasicsManager
        """
        self._log(sInfo.strip())
        
        
        #create notebook
        generate_notebook = kwargs.get('generate_notebook', True)
        
        if generate_notebook:
            saveToFolder = f"differentiation"
            KmTemplateUtil.CREATE_MANAGER_NOTEBOOK("xv.math.calculus", 
                                                   "DifferentiationBasicsManager", 
                                                   saveToFolder = saveToFolder,
                                                   overwrite = False, 
                                                   verbose = self.verbose)

        #get a problem        
        from xv.math.calculus import DifferentiationBasicsManager
        kwargs['manager_class'] = DifferentiationBasicsManager
        kwargs['problem_type'] = None
        kwargs['generate_notebook'] = False
        problemTemplate = self._get_problem_from_a_manager(*args, **kwargs)
        return problemTemplate

        pass


    def _problem_max_min_manager(self, *args, **kwargs):
        """
        @generate_notebook: Default True

        doc_style: xv_doc
        """

        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: 
        creating notebook and getting problem from MaxMinManager
        """
        self._log(sInfo.strip())
        
        
        #create notebook
        generate_notebook = kwargs.get('generate_notebook', True)
        
        if generate_notebook:
            saveToFolder = f"differentiation"
            KmTemplateUtil.CREATE_MANAGER_NOTEBOOK("xv.math.calculus", 
                                                   "MaxMinManager", 
                                                   saveToFolder = saveToFolder,
                                                   overwrite = False, 
                                                   verbose = self.verbose)

        #get a problem        
        from xv.math.calculus import MaxMinManager
        kwargs['manager_class'] = MaxMinManager
        kwargs['problem_type'] = None
        kwargs['generate_notebook'] = False
        problemTemplate = self._get_problem_from_a_manager(*args, **kwargs)
        return problemTemplate

        pass



    def _problem_applied_differentiation_manager(self, *args, **kwargs):
        """
        @generate_notebook: Default True

        doc_style: xv_doc
        """

        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: 
        creating notebook and getting problem from AppliedDifferentiationManager
        """
        self._log(sInfo.strip())
        
        
        #create notebook
        generate_notebook = kwargs.get('generate_notebook', True)
        
        if generate_notebook:
            saveToFolder = f"differentiation"
            KmTemplateUtil.CREATE_MANAGER_NOTEBOOK("xv.math.calculus", 
                                                   "AppliedDifferentiationManager", 
                                                   saveToFolder = saveToFolder,
                                                   overwrite = False, 
                                                   verbose = self.verbose)

        #get a problem        
        from xv.math.calculus import AppliedDifferentiationManager
        kwargs['manager_class'] = AppliedDifferentiationManager
        kwargs['problem_type'] = None
        kwargs['generate_notebook'] = False
        problemTemplate = self._get_problem_from_a_manager(*args, **kwargs)
        return problemTemplate

        pass


    def _problem_derivative_advanced_questions(self, *args, **kwargs):
        """
        
        """
        
        folder_path = "question_bank/math/calculus"
        search_file_paths = [
                "derivative_advanced_questions.txt"
            ]
        
        file_paths = self.get_q_bank_factory().search_question_bank_files(folder_path, search_file_paths)
        return self.get_q_bank_factory().PROBLEM_TEMPLATE_Q_BANK_RANDOM_QUESTION(file_paths, *args, **kwargs)



    def _problem_stirlings_approximation(self, *args, **kwargs):
        """
        
        """
        working_helper = self.base_helper.get_calculus_helper().get_working_helper()
        result = working_helper.get_problem_stirlings_approximation()
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        prob_s = f"""
        {result['prob_s']}
        """
        problemTemplate.add_problem(prob_s.strip(), 'text/html')
        
        
        soln_s = f"""
        {result['soln_s']}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {result['answer_s']}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate

    def _problem_WORKING(self, *args, **kwargs):
        """
        
        """
        working_helper = self.base_helper.get_calculus_helper().get_working_helper()
        result = working_helper.get_problem_WORKING()
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        prob_s = f"""
        {result['prob_s']}
        """
        problemTemplate.add_problem(prob_s.strip(), 'text/html')
        
        
        soln_s = f"""
        {result['soln_s']}
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {result['answer_s']}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate

    