'''
Created on 01-Apr-2021
@author: bthakur
'''
import random
import inspect
import traceback

from sympy.core.expr import UnevaluatedExpr as ueval

from sympy import ( latex, solve, simplify, 
                    pi, Rational,
                    exp, sin, cos, tan,
                    asin, acos, atan,
                    sinh, cosh, tanh,
                    log, exp, sqrt,
                    Eq,
                    symbols, Symbol, Function,
                    expand,
                    Mul, Add,
                    Poly
                    )

from numpy import linspace

from xv.util import number_to_nth, list_to_str
from xv.util import run_with_timeout

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

# _polynomial_manager
# PolynomialManager

        
class PolynomialManager(BaseManager):
    '''
    Class dealing with generial form of polynomials,
    factorization, rank, roots, 
    Taylor and Maclaurin series and their applications

    Example:
        ke = PolynomialManager()
        ke
        
        
        ke.getRandomProblem()
        ke.getRandomProblem(problem_type = 0)
        ke.getRandomProblem(problem_type = 1)
        ...
        
        ke.printProblem()
        ke.printAnswer()
        ke.printSolution()
        ke.printProblemHelp()
        
        from sympy import *
        x = symbols('x')
        func = cos(x)
        ke.initProblem(func)        
        
    '''    
    def __new__(cls, *args, **kwargs):
        instance = super(PolynomialManager, cls).__new__(cls, *args, **kwargs)
        for base_class in PolynomialManager.__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_factorization_of_typical_expressions',

                '_problem_polynomial_expression',
                '_problem_is_expression_polynomial',
                '_problem_rank_of_polynomial',
                '_problem_factors_of_polynomial',
                '_problem_roots_of_polynomial',
                '_problem_roots_of_std_quadratic_expr',
                '_problem_roots_of_quadratic_expr',
                '_problem_std_cubic_expr',
                
                '_problem_polynomial_expression_a_x',
                '_problem_derivation_xn_expr',
                '_problem_polynomial_expression_derivative',
                '_problem_polynomial_expression_derivative_subs',
                '_problem_polynomial_maclaurin_expression',
                '_problem_polynomial_taylor_expression',
                '_problem_polynomial_maclaurin_expression_of_a_function',
                '_problem_polynomial_taylor_expression_of_a_function',
                '_problem_approx_value_of_a_function',
                
                '_problem_math_iit_polynomials',
             ])
        pass


    def _problem_math_iit_polynomials(self, *args, **kwargs):
        """
        
        """
        
        folder_path = "question_bank/math/iit/chapter_wise"
        search_file_paths = [
                "math_iit_polynomials.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_factorization_of_typical_expressions(self, *args, **kwargs):
        """
        Factor single variable algebraic expressions
        
        You can pass your own expr:
        expr: any algebraic expressions of one variable

        Minimum Grade: 3
        Maximum Grade: 6

        doc_style: xv_doc
        """

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


        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()

        expr = kwargs.get('expr', None)
        if not expr is None:
            try:
                x = list(expr.free_symbols)[0]
            except Exception as e:
                problem_s = f"""
                Error: {e}
                Invalid expr = {expr}
                """
                problemTemplate.add_problem(problem_s.strip(), "text/html")
                return problemTemplate
        else:
            x, expr = ph.get_random_factorization_expr()
            pass
        
        
        poly = Poly(expr)
        result = ph.factors_of_polynomial_solution_stmt(poly, x)
        self.expr = expr
        self.x = x
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_items = result['problem_items']
        problem_s = "\n\n".join(problem_items)
        
        problemTemplate.add_problem(problem_s.strip(), "text/html")

        soln_items = result['soln_items']
        soln_s = "\n\n".join(soln_items)
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        useful_eqs = ph.get_useful_eqs()
        useful_eqs_str = [f"$\\displaystyle {latex(item)}$" for item in useful_eqs]
        useful_eqs_str = "\n\n".join(useful_eqs_str)
        
        soln_s = f"""
        <h6>Some useful relations:</h6>
        {useful_eqs_str}
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = result['answer_s']
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass
    
    def _problem_polynomial_expression(self, *args, **kwargs):
        """
        returns Polynomial Expression of given variable or expr
        
        You can pass your own variable x:
        x: any algebraic expressions of one variable

        Minimum Grade: 6
        Maximum Grade: 12

        doc_style: xv_doc
        """

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

        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
        x = kwargs.get('x', None)
        if x is None:
            random_term_exp = ph.get_random_exponent(random.randint(1, 3))
            x = ph.x ** random_term_exp 
            pass

        x_symb = Symbol(f"{latex(x)}")

        eq_std_x = ph.get_polynomial_equation(x = ph.x, evaluate = False)
        eq_in_symbs = ph.get_polynomial_equation(x = x_symb, evaluate = False)
        eq = ph.get_polynomial_equation(x = x)
        
        const_symbols_latex = ph.get_const_symbols_latex(0, 3)
        
        
        statement = f""" Write standard polynomial expression of $\\displaystyle {latex(x)}$."""
        problemTemplate.add_problem(statement, "text/html")

        soln_s = f"""
        Standard polynomial expression of $\\displaystyle {latex(ph.x)}$ is:
        
        $\\displaystyle {latex(eq_std_x)} + \\cdots$
        where $\\displaystyle {const_symbols_latex} \\cdots $ are constant coefficients.
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        if x != ph.x:
            soln_s = f"""
            Therefore, we can write standard polynomial expression of $\\displaystyle 
            {latex(x)}$ as:
            
            $\\displaystyle {latex(eq_in_symbs)} + \\cdots$
            
            $\\displaystyle \\implies  {latex(eq)} + \\cdots$
            """
            problemTemplate.add_solution(soln_s.strip(), 'text/html')
            pass
        
        answer_s = f"""
        $\\displaystyle {latex(eq)} + \\cdots$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    def _problem_is_expression_polynomial(self, *args, **kwargs):
        """
        check if expression is polynomial of a given variable or not
        
        optional inputs:
        expr: any algebraic expressions of one variable
        x: any algebraic expressions of one variable. Default value is x

        Minimum Grade: 6
        Maximum Grade: 12

        doc_style: xv_doc
        """

        
        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()

        x = kwargs.get('x', None)
        if x is None:
            x = ph.x

        x_symb = Symbol(f"{latex(x)}")
            
        expr = kwargs.get('expr', None)
        if expr is None:
            expr = ph.get_random_polyvar_expr(x = x)
    
            if random.choice([True, False]):
                random_term_exp = ph.get_random_exponent(random.randint(1, 3))
                y = ph.x ** random_term_exp 
                #y_symb = Symbol(f"{latex(x)}")
                expr_y = ph.get_random_polyvar_expr(x = y)
                expr = expand(expr + expr_y)
            pass

        
        result = ph.is_polynomial_of_solution_stmt(expr, x)

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

        problem_items = result['problem_items']
        problem_s = "\n\n".join(problem_items)
        
        problemTemplate.add_problem(problem_s.strip(), "text/html")

        soln_items = result['soln_items']
        soln_s = "\n\n".join(soln_items)

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

        answer_s = result['answer_s']
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    def _problem_rank_of_polynomial(self, *args, **kwargs):
        """
        Find rank of a polynomial for a given variable
        
        optional inputs:
        expr: any algebraic expressions of one variable
        x: any algebraic expressions of one variable. Default value is x

        Minimum Grade: 6
        Maximum Grade: 12

        doc_style: xv_doc
        """

        
        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()

        x = kwargs.get('x', None)
        if x is None:
            x = ph.x

        x_symb = Symbol(f"{latex(x)}")
            
        expr = kwargs.get('expr', None)
        if expr is None:
            expr = ph.get_random_polyvar_expr(x = x)
    
            if random.choice([True, False]):
                random_term_exp = ph.get_random_exponent(random.randint(1, 3))
                y = ph.x ** random_term_exp 
                #y_symb = Symbol(f"{latex(x)}")
                expr_y = ph.get_random_polyvar_expr(x = y)
                expr = expand(expr + expr_y)
            pass

        
        result = ph.is_polynomial_of_solution_stmt(expr, x)
        poly = result['poly']
        is_polynomial_of_x = result['is_polynomial_of_x']

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

        
        problem_s = f""" 
        Find rank of $\\displaystyle {latex(x)}$ in $\\displaystyle {latex(expr)}$.
        """
        problemTemplate.add_problem(problem_s.strip(), "text/html")

        #check if it is a polynomial of x or not
        soln_items = result['soln_items']

        if is_polynomial_of_x:
            mod_x = result['mod_x']
            mod_poly = result['mod_poly']
            mod_expr = mod_poly.expr
            
            soln_item = f"""
            Rank of $\\displaystyle {latex(x)} \\text{{ in }} {latex(expr)} $
            
            = Rank of $\\displaystyle {latex(mod_x)} \\text{{ in }} {latex(mod_expr)} $
            
            $\\displaystyle = \\text{{ highest power of }} {latex(mod_x)} 
             \\text{{ in }} {latex(mod_expr)} $ 
            
            $\\displaystyle = {latex(mod_poly.degree())} $ 
            
            Rank is also called degree of the polynomial.
            """
            
            answer_s = f"""
            $\\displaystyle {latex(mod_poly.degree())}$
            """
        else:
            soln_item = f"""
            Hence, the rank of $\\displaystyle 
            {latex(x)} \\text{{ in }} {latex(expr)}
            $ cannot be calculated.
            """
            
            answer_s = f"""
            None
            """
            pass

        soln_items.append(soln_item.strip())

        soln_s = "\n\n".join(soln_items)

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

        
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    def _problem_factors_of_polynomial(self, *args, **kwargs):
        """
        Find factors of an expr
        
        optional inputs:
        expr: any algebraic expressions of one variable

        Minimum Grade: 6
        Maximum Grade: 12

        doc_style: xv_doc
        """
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')


        
        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
            
        expr = kwargs.get('expr', None)
        if not expr is None:
            try:
                x = list(expr.free_symbols)[0]
                poly = Poly(expr)
            except Exception as e:
                problem_s = f"""
                Error: {e}
                Invalid expr = {expr}
                """
                problemTemplate.add_problem(problem_s.strip(), "text/html")
                return problemTemplate
        else:
            x = ph.x
            poly = ph.get_random_polynomial(x = x)
            pass
        
        x_symb = x
        result = ph.factors_of_polynomial_solution_stmt(poly, x)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_items = result['problem_items']
        problem_s = "\n\n".join(problem_items)
        
        problemTemplate.add_problem(problem_s.strip(), "text/html")

        soln_items = result['soln_items']
        soln_s = "\n\n".join(soln_items)

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

        answer_s = result['answer_s']
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

 
    def _problem_roots_of_polynomial(self, *args, **kwargs):
        """
        Find roots of an expr
        
        optional inputs:
        expr: any algebraic expressions of one variable

        Minimum Grade: 6
        Maximum Grade: 12

        doc_style: xv_doc
        """

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

        
        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
            
        expr = kwargs.get('expr', None)
        if not expr is None:
            try:
                x = list(expr.free_symbols)[0]
                poly = Poly(expr)
            except Exception as e:
                problem_s = f"""
                Error: {e}
                Invalid expr = {expr}
                """
                problemTemplate.add_problem(problem_s.strip(), "text/html")
                return problemTemplate
        else:
            x = ph.x
            poly = ph.get_random_polynomial(x = x)
            pass
        
        x_symb = x
        poly_factors = ph.get_polynomial_factors(poly)
        expr = poly.expr

        result = ph.factors_of_polynomial_solution_stmt(poly, x)
        all_roots = ph.get_all_roots(poly)
        
        if not all_roots is None:
            all_roots_str = list_to_str([latex(item) for item in all_roots],
                                         sep = f", \\;")
        else:
            all_roots_str = f"None"

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        problem_s = f""" 
        Find roots of $\\displaystyle {latex(expr)}$.
        """
        problemTemplate.add_problem(problem_s.strip(), "text/html")

        soln_items = [] #result['soln_items']

        factors = result['factors']

        soln_item = f"""
        Solve for roots:
        
        $\\displaystyle {latex(expr)} = 0 $
        
        $\\displaystyle \\implies {latex(factors)} = 0 $
        
        $\\displaystyle \\implies {latex(x)} = {all_roots_str} $ 
        """
        
        answer_s = f"""
        $\\displaystyle {all_roots_str}$
        """

        soln_items.append(soln_item.strip())

        soln_s = "\n\n".join(soln_items)

        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass


    def _problem_roots_of_std_quadratic_expr(self, *args, **kwargs):
        """
        Find roots of a quadratic expr
        
        optional inputs:
        expr: a quadratic expressions of one variable

        Minimum Grade: 6
        Maximum Grade: 12

        doc_style: xv_doc
        """

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

        
        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
            
        x = ph.x
        poly = ph.get_symbolic_quadratic_polynomial(x = x)
        expr = poly.expr

        self.poly = poly
        self.x = x
        
        result_quadratic = ph.solution_to_quadratic_polynomial(poly, x)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        problem_s = f""" 
        Find roots of $\\displaystyle {latex(expr)}$.
        """
        problemTemplate.add_problem(problem_s.strip(), "text/html")

        soln_items = [] #result['soln_items']


        soln_item = f"""
        {result_quadratic['soln_s']}
        """
        soln_items.append(soln_item.strip())


        answer_s = f"""
        {result_quadratic['answer_s']}
        """
        
        soln_s = "\n\n".join(soln_items)

        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass


    def _problem_roots_of_quadratic_expr(self, *args, **kwargs):
        """
        Find roots of a quadratic expr
        
        optional inputs:
        expr: a quadratic expressions of one variable

        Minimum Grade: 6
        Maximum Grade: 12

        doc_style: xv_doc
        """

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

        
        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
            
        expr = kwargs.get('expr', None)
        if not expr is None:
            try:
                x = list(expr.free_symbols)[0]
                poly = Poly(expr)
            except Exception as e:
                sError = f"""{self.__class__.__name__}.{inspect.stack()[0][3]} Error: {e}:
                Invalid expr = {expr}
                """
                self._log(sError)
                self._log(traceback.format_exc())
                problemTemplate.add_problem(sError.strip(), "text/html")
                return problemTemplate
        else:
            x = ph.x
            poly = ph.get_random_quadratic_polynomial(x = x)
            pass
        
        x_symb = x
        
        poly_factors = ph.get_polynomial_factors(poly)
        expr = poly.expr
        
        self.poly = poly
        self.x = x

        result = ph.factors_of_polynomial_solution_stmt(poly, x)
        all_roots = ph.get_all_roots(poly)
        
        if not all_roots is None:
            all_roots_str = list_to_str([latex(item) for item in all_roots],
                                         sep = f", \\;")
        else:
            all_roots_str = f"None"
            
        
        result_quadratic = ph.solution_to_quadratic_polynomial(poly, x)
        
        result_quadratic_by_formula = ph.solution_by_formula_to_quadratic_polynomial(poly, x)

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        problem_s = f""" 
        Find roots of $\\displaystyle {latex(expr)}$.
        """
        problemTemplate.add_problem(problem_s.strip(), "text/html")

        soln_items = [] #result['soln_items']

        factors = result['factors']

        soln_item = f"""
        Solve for roots:
        
        $\\displaystyle {latex(expr)} = 0 $
        
        $\\displaystyle \\implies {latex(factors)} = 0 $
        
        $\\displaystyle \\implies {latex(x)} = {all_roots_str} $ 
        
        """
        soln_items.append(soln_item.strip())


        soln_item = f"""
        <h3>Quadratic Solution</h3>
        {result_quadratic['soln_s']}
        
        <h3>Using Quadratic Solution Formula</h3>
        {result_quadratic_by_formula['soln_s']}
        
        """
        soln_items.append(soln_item.strip())


        answer_s = f"""
        {result_quadratic['answer_s']}
        """
        
        soln_s = "\n\n".join(soln_items)

        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass


    def _problem_std_cubic_expr(self, *args, **kwargs):
        """
        standard cubic expression 
        

        doc_style: xv_doc
        """

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

        
        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
            
        x = ph.x
        cubic_eq = ph.get_standard_cubic_eq(x = x)
        
        self.cubic_eq = cubic_eq
        self.x = x
        
        result_cubic_eq = ph.solution_to_cubic_eq(cubic_eq, x)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        problem_s = f""" 
        Find roots of $\\displaystyle {latex(cubic_eq)}$.
        """
        problemTemplate.add_problem(problem_s.strip(), "text/html")

        soln_items = [] 
        

        soln_item = f"""
        {result_cubic_eq['soln_s']}
        """
        soln_items.append(soln_item.strip())


        answer_s = f"""
        {result_cubic_eq['answer_s']}
        """
        
        soln_s = "\n\n".join(soln_items)

        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass


    def _problem_polynomial_expression_a_x(self, *args, **kwargs):
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
        x = ph.x
        a = ph.a
        eq = ph.get_polynomial_equation(x = x, a = a)
        
        statement = f""" Write polynomial expression of ${ph.readable_latex(x - a)}$."""
        problemTemplate.add_problem(statement, "text/html")

        soln_s = f"""
        Polynomial expression of $({ph.readable_latex(x - a)})$ is:
        $\\displaystyle {ph.readable_latex(eq)} + \\cdots$
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        $\\displaystyle {ph.readable_latex(eq)} + \\cdots$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass


    def _problem_polynomial_expression_derivative(self, *args, **kwargs):
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
        n = random.randint(1, 4)
        x = ph.x
        a = random.choice([0, ph.a])
        eq = ph.get_polynomial_equation(x = x, a = a)

        derivative_series = ph.get_derivative_series(eq,
                                x = x, n = n)
        
        derivative_series_str = ph.get_derivative_series_str(
                                        derivative_series)
        
        answer = derivative_series[-1]
        
        statement = f""" 
        Find {number_to_nth(n)} derivative of  polynomial expression of ${ph.readable_latex(x- a)}$."""
        problemTemplate.add_problem(statement, "text/html")

        soln_s = f"""
        $\\displaystyle {ph.readable_latex(eq)} + \\cdots$
        
        {derivative_series_str}
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        $\\displaystyle {ph.readable_latex(answer)} + \\cdots$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass
   
    def _problem_derivation_xn_expr(self, *args, **kwargs):
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
        n = random.randint(1, 4)
        x = ph.x
        a = random.choice([0, ph.a])
        eq = ph.get_polynomial_equation(x = x, a = a)
        
        x_val = random.randint(0, 5)

        derivative_series = ph.get_derivative_series(eq,
                                x = x, n = n)
        
        derivative_series_str = ph.get_derivative_series_str(
                                        derivative_series)
        
        answer_expr = derivative_series[-1]
        answer = answer_expr.subs({x: x_val})
        
        statement = f""" 
        Find {number_to_nth(n)} derivative of 
        polynomial expression of ${ph.readable_latex(x- a)}$ at ${latex(x)} = {x_val}$."""
        problemTemplate.add_problem(statement, "text/html")

        soln_s = f"""
        $\\displaystyle {ph.readable_latex(eq)} + \\cdots$
        
        {derivative_series_str}
        
        Substituting the value ${latex(x)} = {latex(x_val)}$:
        
        $\\displaystyle {latex(answer)}$
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

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

    def _problem_polynomial_expression_derivative_subs(self, *args, **kwargs):
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
        n = random.randint(1, 4)
        x = ph.x
        a = random.choice([0, ph.a])
        eq = ph.get_polynomial_equation(x = x, a = a)
        
        x_val = random.randint(0, 5)

        derivative_series = ph.get_derivative_series(eq,
                                x = x, n = n)
        
        derivative_series_str = ph.get_derivative_series_str(
                                        derivative_series)
        
        answer_expr = derivative_series[-1]
        answer = answer_expr.subs({x: x_val})
        
        statement = f""" 
        Find {number_to_nth(n)} derivative of 
        polynomial expression of ${ph.readable_latex(x- a)}$ at ${latex(x)} = {x_val}$."""
        problemTemplate.add_problem(statement, "text/html")

        soln_s = f"""
        $\\displaystyle {ph.readable_latex(eq)} + \\cdots$
        
        {derivative_series_str}
        
        Substituting the value ${latex(x)} = {latex(x_val)}$:
        
        $\\displaystyle {latex(answer)}$
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

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

    def _problem_polynomial_maclaurin_expression(self, *args, **kwargs):
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
        n = 5
        x = ph.x
        a = 0
        x_val = a
        
        #result = ph.compute_standard_taylor_series(x, a, n, x_val)
        timeout = 5
        result = run_with_timeout(
                          f'Thread_compute_standard_taylor_series', 
                          ph.compute_standard_taylor_series, 
                          (x, a, n), 
                          timeout = timeout, 
                          verbose = self.verbose)

        if not result:
            statement = f""" 
            Failed to compute Maclaurin series of polynomial of ${latex(x)}$ 
            """
            problemTemplate.add_problem(statement, "text/html")
            return problemTemplate
        
        
        statement = f""" 
        Derive Maclaurin series of ${ph.readable_latex(result['eq'].lhs)}$"""
        problemTemplate.add_problem(statement, "text/html")

        soln_s = f"""
        $\\displaystyle {result['eq_latex']} + \\cdots$
        
        Calculate derivatives with respect to ${latex(x)}$
        
        {result['derivative_series_str']}
        $\\cdots$
        
        Substituting the value ${latex(x)} = {latex(x_val)}$:
        
        {result['derivative_series_subs_str']}
        $\\cdots$
        
        Compute the constants:
        
        {result['const_solns_str']}
        $\\cdots$
        
        Rewrite original polynomial function with the computed constant values:

        $\\displaystyle {result['eq_latex']} + \\cdots$
        
        $\\displaystyle {result['rewitten_eq_latex']} + \\cdots$
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        $\\displaystyle {result['rewitten_eq_latex']} + \\cdots$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass



    def _problem_polynomial_taylor_expression(self, *args, **kwargs):
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
        n = 5
        x = ph.x
        a = ph.a
        x_val = ph.a
        
        #result = ph.compute_standard_taylor_series(x, a, n, x_val)
        timeout = 5
        result = run_with_timeout(
                          f'Thread_compute_standard_taylor_series', 
                          ph.compute_standard_taylor_series, 
                          (x, a, n), 
                          timeout = timeout, 
                          verbose = self.verbose)

        if not result:
            statement = f""" 
            Failed to compute Taylor series of $({latex(x)} - {latex(a)})$ 
            """
            problemTemplate.add_problem(statement, "text/html")
            return problemTemplate
        
        
        statement = f""" 
        Derive Taylor series of ${ph.readable_latex(result['eq'].lhs)}$"""
        problemTemplate.add_problem(statement, "text/html")

        soln_s = f"""
        $\\displaystyle {result['eq_latex']} + \\cdots$
        
        Calculate derivatives with respect to ${latex(x)}$
        
        {result['derivative_series_str']}
        $\\cdots$
        
        Substituting the value ${latex(x)} = {latex(x_val)}$:
        
        {result['derivative_series_subs_str']}
        $\\cdots$
        
        Compute the constants:

        {result['const_solns_str']}
        $\\cdots$
        
        Rewrite original polynomial function with the computed constant values:

        $\\displaystyle {result['eq_latex']} + \\cdots$
        
        $\\displaystyle {result['rewitten_eq_latex']} + \\cdots$
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        $\\displaystyle {result['rewitten_eq_latex']} + \\cdots$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    def _problem_polynomial_maclaurin_expression_of_a_function(self, *args, **kwargs):
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
        n = 5
        x = ph.x
        
        func, a = ph.get_random_function_for_series(maclaurin = True)
        x_val = a

        #result = ph.compute_taylor_series_for_a_func(func, x, a, n, x_val)

        timeout = 5
        result = run_with_timeout(
                          f'Thread_compute_taylor_series_for_a_func', 
                          ph.compute_taylor_series_for_a_func, 
                          (func, x, a, n, x_val), 
                          timeout = timeout, 
                          verbose = self.verbose)

        if not result:
            statement = f""" 
            Failed to compute Maclaurin series of ${latex(func)}$ at ${latex(x)} = {latex(x_val)}$
            """
            problemTemplate.add_problem(statement, "text/html")
            return problemTemplate
        
        
        statement = f""" 
        Derive Maclaurin series of ${result['func_latex']}$"""
        problemTemplate.add_problem(statement, "text/html")

        soln_s = f"""
        $\\displaystyle {result['eq_applied_latex']}$
        
        Calculate derivatives with respect to ${latex(x)}$
        
        {result['derivative_series_str']}
        $\\cdots$
        
        Substituting the value ${latex(x)} = {latex(x_val)}$:
        
        {result['derivative_series_subs_str']}
        $\\cdots$
        
        Rewrite the standard Maclaurin series with the computed values:

        $\\displaystyle {result['std_series_equation_latex']} + \\cdots$
        
        $\\displaystyle \\implies {result['rewitten_eq_latex']} + \\cdots$

        $\\displaystyle \\implies {result['rewitten_eq_final_latex']} + \\cdots$
        
        In simplified format:
        
        $\\displaystyle \\implies {latex(result['answer'])} + \\cdots$
        
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        $\\displaystyle {result['rewitten_eq_final_latex']} + \\cdots$

        In simplified format:
        
        $\\displaystyle {latex(result['answer'])} + \\cdots$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    def _problem_polynomial_taylor_expression_of_a_function(self, *args, **kwargs):
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
        n = 5
        x = ph.x
        
        func, a = ph.get_random_function_for_series()
        x_val = a

        #result = ph.compute_taylor_series_for_a_func(func, x, a, n, x_val)

        timeout = 5
        result = run_with_timeout(
                          f'Thread_compute_taylor_series_for_a_func', 
                          ph.compute_taylor_series_for_a_func, 
                          (func, x, a, n, x_val), 
                          timeout = timeout, 
                          verbose = self.verbose)

        if not result:
            statement = f""" 
            Failed to compute Taylor series of ${latex(func)}$ at ${latex(x)} = {latex(x_val)}$
            """
            problemTemplate.add_problem(statement, "text/html")
            return problemTemplate
        
        
        statement = f""" 
        Derive Taylor series of ${result['func_latex']}$"""
        problemTemplate.add_problem(statement, "text/html")

        soln_s = f"""
        $\\displaystyle {result['eq_applied_latex']}$
        
        Calculate derivatives with respect to ${latex(x)}$
        
        {result['derivative_series_str']}
        $\\cdots$
        
        Substituting the value ${latex(x)} = {latex(x_val)}$:
        
        {result['derivative_series_subs_str']}
        $\\cdots$
        
        Rewrite the standard Taylor series with the computed values:

        $\\displaystyle {result['std_series_equation_latex']} + \\cdots$
        
        $\\displaystyle \\implies {result['rewitten_eq_latex']} + \\cdots$

        $\\displaystyle \\implies {result['rewitten_eq_final_latex']} + \\cdots$
        
        In simplified format:
        
        $\\displaystyle \\implies {latex(result['answer'])} + \\cdots$
        
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        $\\displaystyle {result['rewitten_eq_final_latex']} + \\cdots$

        In simplified format:
        
        $\\displaystyle {latex(result['answer'])} + \\cdots$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    def _problem_approx_value_of_a_function(self, *args, **kwargs):
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        ph = self.base_helper.get_algebra_helper().get_polynomial_helper()
        n = 5
        x = ph.x
        
        func, a = ph.get_random_function_for_series()
        x_val = random.choice([1, 2, pi, pi/2, pi/3, pi/4, pi/6])

        
        timeout = 5
        #taylor_series_result = ph.compute_taylor_series_for_a_func(
        #                                func, x, x_val, n, x_val)
        
        taylor_series_result = run_with_timeout(
                          f'Thread_compute_taylor_series_for_a_func', 
                          ph.compute_taylor_series_for_a_func, 
                          (func, x, x_val, n, x_val), 
                          timeout = timeout, 
                          verbose = self.verbose)

        if not taylor_series_result:
            statement = f""" 
            Failed to compute Taylor series of ${latex(func)}$ at ${latex(x)} = {latex(x_val)}$
            """
            problemTemplate.add_problem(statement, "text/html")
            return problemTemplate
        
        
        
        #result = ph.compute_approx_value_of_a_func(func, x, n, x_val)
        result = run_with_timeout(
                          f'Thread_compute_approx_value_of_a_func', 
                          ph.compute_approx_value_of_a_func, 
                          (func, x, n, x_val), 
                          timeout = timeout, 
                          verbose = self.verbose)

        if not result:
            statement = f""" 
            Failed to compute value of ${latex(func)}$ at ${latex(x)} = {latex(x_val)}$
            """
            problemTemplate.add_problem(statement, "text/html")
            return problemTemplate
        
        
        svg_str = ph.plot_taylor_expr(func, 
                                      result['taylor_expr'],
                                      x,
                                      x_val,
                                      use_cache = True)

        
        statement = f""" 
        Find approximate value of ${latex(func)}$ at $({latex(x)} = {latex(x_val)})$
        """
        problemTemplate.add_problem(statement, "text/html")

        

        soln_s = f"""
        $\\displaystyle {taylor_series_result['eq_applied_latex']}$
        
        The expanded Taylor series is using a factor $({latex(x)} - {latex(x_val)})$

        $\\displaystyle {taylor_series_result['std_series_equation_latex']} + \\cdots$


        Calculate derivatives with respect to ${latex(x)}$
        
        {taylor_series_result['derivative_series_str']}
        $\\cdots$
        
        Substituting the value ${latex(x)} = {latex(x_val)}$:
        
        {taylor_series_result['derivative_series_subs_str']}
        $\\cdots$
        
        
        $\\displaystyle \\implies {taylor_series_result['rewitten_eq_final_latex']} + \\cdots$

        In simplified format of Taylor series:
        
        $\\displaystyle {latex(func)} = {latex(result['taylor_expr_full'])}$
        
        Approx Value from Taylor Series by substituting $({latex(x)} = {latex(x_val)})$
        
        $\\displaystyle 
        {latex(result['taylor_value'])} = {result['taylor_value_float']}$
        
        Actual Value $\\displaystyle 
        = {latex(result['actual_value'])} = {latex(result['actual_value_float'])}$
        
        Error in computation $\\displaystyle 
        = abs({latex(result['actual_value_float'])} -  {latex(result['taylor_value_float'])})
        = {latex(result['error_value'])}
        $
        
        
        % error in computation $\\displaystyle 
        = {result['error_percent_latex']}
        = {latex(result['error_percent'])}
        $
        
        """
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        problemTemplate.add_solution(svg_str, 'text/svg')

        answer_s = f"""
        $\\displaystyle {latex(result['taylor_value'])} = {result['taylor_value_float']}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

