'''
Created on 20-Jan-2022

@author: bthakur
'''

import codecs
import inspect
import traceback
import random
import numpy as np
import sympy as sp
from sympy import (latex, Symbol, symbols,
                    Rational, Poly,
                    sin, cos, tan,
                    asin, acos, atan,
                    sinh, cosh, tanh,
                    log, exp, sqrt,
                   diff, factor,
                   Function, Eq,
                   Add, Mul, Pow, expand,
                   simplify, solve, factorial,
                   ComplexRootOf
                   )

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

from xv.km.util import latex_fraction
from sympy import UnevaluatedExpr as ueval

from xv.km.ui import SvgHelper
from xv.util.data import CacheUtil

import matplotlib.pyplot as plt


from xv.util import UtilClass, run_with_timeout

class PolynomialHelper(UtilClass):
    '''
    Class dealing with generial form of polynomials,
    factorization, rank, roots,
    Taylor and Maclaurin series and their applications
    '''
    _MAX_ROOT_CAL_TIME = 15

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.w = Symbol('w')
        self.x = Symbol('x')
        self.y = Symbol('y')
        self.a = Symbol('a', constant = True)
        self._set_terms(7)
        self._init_data()
        pass

    def compute_approx_value_of_a_func(self, func, x, n, x_val):
        #taylor_expr_full = series(rhs_expr, x, x_val , n)

        taylor_expr_full = series(func, x, x_val , n)
        taylor_expr = taylor_expr_full.removeO()

        actual_value = func.subs({x: x_val})
        taylor_value = taylor_expr.subs({x: x_val})
        error_value = abs(actual_value.n() - taylor_value.n())

        error_percent = error_value / actual_value.n()
        error_percent_latex = latex_fraction(error_value, actual_value.n())

        result = {
            'taylor_expr_full': taylor_expr_full,
            'taylor_expr': taylor_expr,
            'taylor_value': taylor_value,
            'taylor_value_float': taylor_value.n(),
            'actual_value': actual_value,
            'actual_value_float': actual_value.n(),
            'error_value': error_value,
            'error_percent': error_percent,
            'error_percent_latex': error_percent_latex
            }

        return result

        pass

    def get_real_roots(self, poly):
        try:
            timeout = self.__class__._MAX_ROOT_CAL_TIME
            roots = run_with_timeout(
                              f'Thread_real_roots',
                              poly.real_roots,
                              None,
                              timeout = timeout,
                              verbose = self.verbose)
            return roots
        except Exception as e:
            sError = f"""{self.__class__.__name__}.{inspect.stack()[0][3]} Error: {e}:
            poly = {poly}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            return None
        pass

    def get_useful_eqs(self):
        useful_eqs = []

        x = self.x
        a = Symbol('y')
        b = Symbol('z')


        expr = (x + a)**2
        useful_eq = Eq(expr, ueval(expand(expr)))
        useful_eqs.append(useful_eq)

        expr = (x - a)**2
        useful_eq = Eq(expr, ueval(expand(expr)))
        useful_eqs.append(useful_eq)

        expr = (x + a)**3
        useful_eq = Eq(expr, ueval(expand(expr)))
        useful_eqs.append(useful_eq)

        expr = (x - a)**3
        useful_eq = Eq(expr, ueval(expand(expr)))
        useful_eqs.append(useful_eq)

        expr = x**2 - a**2
        useful_eq = Eq(expr, ueval(factor(expr)))
        useful_eqs.append(useful_eq)

        expr = (x**2 + x * a + a ** 2) * (x - a)
        useful_eq = Eq(ueval(expand(expr)), expr)
        useful_eqs.append(useful_eq)

        expr = (x**3 + x**2 * a + x * a**2 + a ** 3) * (x - a)
        useful_eq = Eq(ueval(expand(expr)), expr)
        useful_eqs.append(useful_eq)

        expr = (x - a * sp.I) * (x + a * sp.I)
        useful_eq = Eq(ueval(expand(expr)), expr)
        useful_eqs.append(useful_eq)


        expr = (x + a) * (x + b)
        useful_eq = Eq(ueval(expand(expr)), expr)
        useful_eqs.append(useful_eq)

        return useful_eqs


    def get_all_factors(self, poly, x):
        timeout = 5
        all_factors = run_with_timeout(
                          f'Thread_get_all_factors',
                          self._get_all_factors,
                          (poly, x),
                          timeout = timeout,
                          verbose = self.verbose)
        if not all_factors is None:
            return all_factors

        all_factors = sp.factor(poly.expr)

        if not isinstance(all_factors, Mul):
            all_factors = [all_factors]
        else:
            all_factors = all_factors.args
            pass

        return all_factors
        pass

    def _get_all_factors(self, poly, x):
        all_roots = self.get_all_roots(poly)

        factors = []
        for root_item in all_roots:
            if isinstance(root_item, ComplexRootOf):
                return None
            factors.append(x - root_item)
            pass


        poly_calc = Mul(*factors)
        poly_calc = simplify(poly_calc)
        poly_calc = expand(poly_calc)

        if poly_calc == poly.expr:
            return factors
            pass

        try:
            mult_factor = simplify(poly.expr / poly_calc)

            factors.append(mult_factor)
            poly_calc = Mul(*factors)
            poly_calc = simplify(poly_calc)
            poly_calc = expand(poly_calc)
            if poly_calc == poly.expr:
                return factors
                pass
            pass
        except Exception as e:
            sError = f"""{self.__class__.__name__}.{inspect.stack()[0][3]} Error: {e}:
            poly = {poly}
            """
            self._log(sError)
            self._log(traceback.format_exc())

        self._log(f"""
        factors are not same as expr:
        poly.expr = {poly.expr}
        poly_calc = {poly_calc}
        """)
        return None
        pass

    def get_all_roots(self, poly):
        try:

            timeout = self.__class__._MAX_ROOT_CAL_TIME
            roots = run_with_timeout(
                              f'Thread_all_roots',
                              poly.all_roots,
                              None,
                              timeout = timeout,
                              verbose = self.verbose)

            return roots
        except Exception as e:
            sError = f"""{self.__class__.__name__}.{inspect.stack()[0][3]} Error: {e}:
            poly = {poly}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            return None
        pass

    def subs_poly(self, poly, x, mod_x, max_exponent = 20):
        if str(x) == str(mod_x):
            return poly

        expr = poly.expr

        for i in range(max_exponent, 0, -1):
            repl_dict = {x ** i: mod_x ** i}
            repl_dict = {x ** -i: mod_x ** -i}

            #print(f"Before expr = {expr}")
            #print(f"repl_dict = {repl_dict}")

            expr = expr.subs(repl_dict)
            #print(f"After expr = {expr}")
            pass

        return Poly(expr)
        pass

    def is_polynomial_of_solution_stmt(self, expr, x):
        if hasattr(x, 'free_symbols'):
            x_free_symb = list(x.free_symbols)[0]
        else:
            x_free_symb = x

        f = Function('f')(x_free_symb)
        poly = self.convert_to_polynomial(expr)
        is_polynomial_of_x, is_polynomial_msg = self.is_polynomial_of(poly, x)

        problem_items = []
        soln_items = []

        problem_item = f"""
        Is expression a polynomial of $\\displaystyle {latex(x)}$?

        $\\displaystyle {latex(expr)}$

        """
        problem_items.append(problem_item.strip())


        soln_item = f"""
        The given expression is:
        $\\displaystyle {latex(f)} = {latex(expr)}$
        """

        soln_items.append(soln_item.strip())

        if str(x) != str(self.x):
            mod_x = Symbol('y')
            mod_f = Function('f')(mod_x)
            mod_poly = self.subs_poly(poly, x, mod_x, max_exponent = 20)

            soln_item = f"""
            Let us try to rewrite it by replacing $\\displaystyle
            {latex(x)} \\text{{ with }} {latex(mod_x)} $

            $\\displaystyle {latex(mod_f)} = {latex(mod_poly.expr)} $

            """
            soln_items.append(soln_item.strip())
        else:
            mod_x = x
            mod_f = f
            mod_poly = poly
            pass


        if is_polynomial_of_x:
            soln_item = f"""
            As we see, all terms of $\\displaystyle {latex(mod_f)}
            $ have non-negative integer powers of $\\displaystyle
            {latex(mod_x)} $
            Therefore, $\\displaystyle {latex(mod_f)}
            $ is a polynomial of $\\displaystyle {latex(mod_x)}$.
            """
            soln_items.append(soln_item.strip())

            if str(x) != str(self.x):
                soln_item = f"""
                $\\displaystyle \\implies {latex(f)}
                $ is a polynomial of $\\displaystyle {latex(x)}$.
                """
                soln_items.append(soln_item.strip())

            soln_item = f"""
            The polynomial form is:
            $\\displaystyle {latex(f)} = {latex(poly)}$
            """
            soln_items.append(soln_item.strip())

            answer_s = f"""Yes"""
            pass
        else:
            soln_item = f"""
            As we see, all terms of $\\displaystyle {latex(mod_f)}
            $ cannot be expressed as non-negative integer powers of $\\displaystyle
            {latex(mod_x)} $
            Therefore, $\\displaystyle {latex(mod_f)}
            $ is a not polynomial of $\\displaystyle {latex(mod_x)}$.
            """
            soln_items.append(soln_item.strip())


            if str(x) != str(self.x):
                soln_item = f"""
                $\\displaystyle \\implies {latex(f)}
                $ is not a polynomial of $\\displaystyle {latex(x)}$.
                """
                soln_items.append(soln_item.strip())

            answer_s = f"""No"""
            pass


        result = {
            'expr': expr,
            'x': x,
            'poly': poly,
            'f': f,
            'mod_x': mod_x,
            'mod_f': mod_f,
            'mod_poly': mod_poly,
            'is_polynomial_of_x': is_polynomial_of_x,
            'is_polynomial_msg': is_polynomial_msg,
            'problem_items': problem_items,
            'soln_items': soln_items,
            'answer_s': answer_s,
            }

        return result
        pass

    def get_random_factorization_expr(self):
        x = self.x
        a = random.randint(1, 10) * random.choice([1, -1])
        b = random.randint(1, 10) * random.choice([1, -1])
        c = random.randint(1, 10) * random.choice([1, -1])

        a_or_1 = random.choice([a, 1])
        b_or_1 = random.choice([b, 1])
        c_or_1 = random.choice([c, 1])

        a = random.choice([Symbol('m', real = True), a])
        b = random.choice([Symbol('n', real = True), b])
        c = random.choice([Symbol('p', real = True), c])

        exprs = []

        expr = 125 * x**3 - 25 * x**4 + 50 * x**2
        exprs.append(expr)

        expr = 3 * x * (x**2 - 7) - 4 * (7 - x**2)
        exprs.append(expr)

        expr = (x + a)**2
        exprs.append(expr)


        expr = (x + a)**3
        exprs.append(expr)

        expr = (x - a)**2
        exprs.append(expr)

        expr = (x - a)**3
        exprs.append(expr)

        expr = (x + a) * (x + b)
        exprs.append(expr)

        expr = (- x + a) * (x + b)
        exprs.append(expr)


        expr = x**2 * b_or_1**2 + a**2 * c_or_1**2
        exprs.append(expr)

        expr = x**2 * b_or_1**2 - a**2 * c_or_1**2
        exprs.append(expr)

        expr = x**3 * b_or_1**3 + a**3 * c_or_1**3
        exprs.append(expr)

        expr = x**3 * b_or_1**3 - a**3 * c_or_1**3
        exprs.append(expr)


        expr = (x + a) * (x + b) * (x + c)
        expr = expand(expr)
        exprs.append(expr)

        expr = (x * b_or_1 + a * c_or_1)**3
        expr = expand(expr)
        exprs.append(expr)

        expr = (x * b_or_1 - a * c_or_1)**3
        expr = expand(expr)
        exprs.append(expr)


        expr = random.choice(exprs)
        if random.choice([True, False]):
            expr = expr * c

        poly = Poly(expr)

        return x, expr
        pass


    def factors_of_polynomial_solution_stmt(self, poly, x):
        poly_factors = self.get_polynomial_factors(poly)
        all_factors = self.get_all_factors(poly, x)

        expr = poly.expr

        problem_items = []
        soln_items = []

        problem_item = f"""
        Find factors of $\\displaystyle {latex(expr)}$.
        """
        problem_items.append(problem_item.strip())


        if not all_factors is None:
            all_factors_symbs = [self._get_symb_with_bracket(item, pow_bracket = True)
                                 for item in all_factors]

            all_factors_symbs = Mul(*all_factors_symbs)

            soln_item = f"""
            Factors of $\\displaystyle {latex(expr)} $

            $\\displaystyle = {latex(poly_factors)} $
            """
            soln_items.append(soln_item.strip())

            if latex(poly_factors) != latex(all_factors_symbs):
                soln_item = f"""
                $\\displaystyle = {latex(all_factors_symbs)} $
                """
                soln_items.append(soln_item.strip())
                pass

            factors = all_factors_symbs

        else:
            soln_item = f"""
            Factors of $\\displaystyle {latex(expr)} $

            $\\displaystyle = {latex(poly_factors)} $
            """
            soln_items.append(soln_item.strip())

            answer_s = f"""
            $\\displaystyle {latex(poly_factors)}$
            """
            factors = poly_factors

            pass

        answer_s = f"""
        $\\displaystyle {latex(factors)} $
        """

        result = {
            'expr': expr,
            'x': x,
            'poly': poly,
            'factors': factors,
            'problem_items': problem_items,
            'soln_items': soln_items,
            'answer_s': answer_s,
            }

        return result
        pass

    def solution_to_quadratic_polynomial(self, poly, x):
        expr = poly.expr

        soln_items = []

        a = poly.expr.coeff(x**2)
        b = poly.expr.coeff(x**1)
        c = poly.expr.subs(x, 0)

        eq_expr = Eq(expr, 0)


        soln_item = f"""
        $\\displaystyle

        \\begin {{aligned}}

        & {latex(eq_expr)}
        \\\\ \\\\
        """
        soln_items.append(soln_item.strip())

        eq0 = Eq(x**2 + b / a * x**1 + c / a, 0)

        if latex(eq0) != latex(eq_expr):
            soln_item = f"""
            \\implies
            & {latex(eq0)}
            \\qquad \\text{{ divided both sides by }} {latex(a)}
            \\\\ \\\\
            """
            soln_items.append(soln_item.strip())

        b_2a_ueval = Symbol(f"\\frac{{ {latex(b)} }} {{ 2 \\cdot {latex(a)} }}")
        eq1_term2 = Symbol(f"2 \\cdot {latex(x)} \\cdot {latex(b_2a_ueval)} ")
        eq1_term3 = Symbol(f" \\left( {latex(b_2a_ueval)} \\right)^2 ")

        eq1_lhs = Symbol(f"""
        {latex(x**2)} + {latex(eq1_term2)} + {latex(eq1_term3)} - {latex(eq1_term3)}
        + {latex(c / a)}
        """)
        eq1 = Eq(eq1_lhs, 0)

        if latex(eq1) != latex(eq0):
            soln_item = f"""
            \\implies
            & {latex(eq1)}
            \\qquad \\text{{ used first two terms to make a perfect square in form of }}
            a^2 + 2 a b + b^2
            \\text{{ in expanded form }}
            \\\\ \\\\
            """
            soln_items.append(soln_item.strip())


        b_2a = b / (2 * a)


        eq2_term2 = Symbol(f"2 \\cdot {latex(x)} \\cdot {latex(b_2a)} ")
        eq2_term3 = Symbol(f" \\left( {latex(b_2a)} \\right)^2 ")

        eq2_lhs = Symbol(f"""
        {latex(x**2)} + {latex(eq2_term2)} + {latex(eq2_term3)} - {latex(eq2_term3)}
        + {latex(c / a)}
        """)
        eq2 = Eq(eq2_lhs, 0)

        if latex(eq2) != latex(eq1):
            soln_item = f"""
            \\implies
            & {latex(eq2)}
            \\\\ \\\\
            """
            soln_items.append(soln_item.strip())


        eq3_term1 = Symbol(f"{{ \\left( {latex(x)} + {latex(b_2a)} \\right) }}^2" )
        eq3_lhs = Symbol(f"""
        {latex(eq3_term1)} - {latex(eq2_term3)}
        + {latex(c / a)}
        """)
        eq3 = Eq(eq3_lhs, 0)

        if latex(eq3) != latex(eq2):
            soln_item = f"""
            \\implies
            & {latex(eq3)}
            \\qquad \\text{{ wrote perfect square in form of }} ( a + b )^2
            \\\\ \\\\
            """
            soln_items.append(soln_item.strip())


        eq4_term2 = Symbol(f"{latex((b / (2 * a))**2)}" )


        eq4_lhs = Symbol(f"""
        {latex(eq3_term1)} - {latex(eq4_term2)}
        + \\frac {{ {latex(4 * a * c)} }}
                 {{ {latex(4 * a * a)} }}
        """)
        eq4 = Eq(eq4_lhs, 0)

        if latex(eq4) != latex(eq3):
            soln_item = f"""
            \\implies
            & {latex(eq4)}
            \\\\ \\\\
            """
            soln_items.append(soln_item.strip())



        eq5_term2 = Symbol(f"""
        \\frac {{ {latex(b ** 2)} - {latex(4 * a * c) } }}
               {{ {latex( 4 * a**2 )} }}
        """)
        eq5_lhs = Symbol(f"""
        {latex(eq3_term1)} - \\left( {latex(eq5_term2)} \\right)
        """)
        eq5 = Eq(eq5_lhs, 0)



        if latex(eq5) != latex(eq4):
            soln_item = f"""
            \\implies
            & {latex(eq5)}
            \\\\ \\\\
            """
            soln_items.append(soln_item.strip())


        eq6_rhs = eq5_term2
        eq6 = Eq(eq3_term1, eq6_rhs)


        if latex(eq6) != latex(eq5):
            soln_item = f"""
            \\implies
            & {latex(eq6)}
            \\\\ \\\\
            """
            soln_items.append(soln_item.strip())


        eq7_lhs = Symbol(f"{latex(x)} + {latex(b_2a)}" )
        eq7_rhs = Symbol(f"\\pm \\sqrt {{ {latex(eq6_rhs)} }}")
        eq7 = Eq(eq7_lhs, eq7_rhs)


        if latex(eq7) != latex(eq6):
            soln_item = f"""
            \\implies
            & {latex(eq7)}
            \\qquad \\text{{ took square root of both sides }}
            \\\\ \\\\
            """
            soln_items.append(soln_item.strip())

        eq8_rhs_term2 = Symbol(f"""
        \\frac {{ \\sqrt{{ {latex(b ** 2)} - {latex(4 * a * c) } }} }}
               {{ {latex( 2 * a )} }}
        """)

        eq8_rhs = Symbol(f"""
        {latex(- b / (2 * a))} \\pm {latex(eq8_rhs_term2)}
        """)
        eq8 = Eq(x, eq8_rhs)


        if latex(eq8) != latex(eq7):
            soln_item = f"""
            \\implies
            & {latex(eq8)}
            \\\\ \\\\
            """
            soln_items.append(soln_item.strip())

        soln_ueval_1 = Symbol(f"{latex(- b / (2 * a))} + {latex(eq8_rhs_term2)}")
        soln_ueval_2 = Symbol(f"{latex(- b / (2 * a))} - {latex(eq8_rhs_term2)}")

        soln_item = f"""
        \\implies
        & {latex(x)} =

        \\begin {{cases}}

        & {latex(soln_ueval_1)} \\\\ \\\\
        & {latex(soln_ueval_2)}

        \\end {{cases}}

        \\\\ \\\\
        """
        soln_items.append(soln_item.strip())

        soln_eval_term2 = sp.sqrt(b**2 - 4 * a * c) / (2 * a)

        soln_eval_1 = - b / (2 * a) + soln_eval_term2
        soln_eval_2 = - b / (2 * a) - soln_eval_term2

        soln_item = f"""
        \\implies
        & {latex(x)} =

        \\begin {{cases}}

        & {latex(soln_eval_1)} \\\\ \\\\
        & {latex(soln_eval_2)}

        \\end {{cases}}

        \\\\ \\\\
        """
        soln_items.append(soln_item.strip())






        soln_item = f"""
        \\end {{aligned}}
        $
        """
        soln_items.append(soln_item.strip())

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

        answer_s = f"""
        $\\displaystyle
        {latex(soln_eval_1)} , \\;
        {latex(soln_eval_2)}
        $
        """

        result = {
            'expr': expr,
            'x': x,
            'poly': poly,
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result
        pass



    def solution_by_formula_to_quadratic_polynomial(self, poly, x):
        expr = poly.expr

        soln_items = []

        a = poly.coeff_monomial(x ** 2)
        b = poly.coeff_monomial(x ** 1)
        c = poly.coeff_monomial(x ** 0)

        eq_expr = Eq(expr, 0)


        soln_item = f"""
        $\\displaystyle

        \\begin {{aligned}}

        """
        soln_items.append(soln_item.strip())


        soln_item = f"""
        & {latex(eq_expr)}
        \\\\ \\\\

        & \\implies

        \\begin {{cases}}

        & a = {latex(a)} \\\\ \\\\
        & b = {latex(b)} \\\\ \\\\
        & c = {latex(c)}

        \\end {{cases}}
        \\\\ \\\\
        """
        soln_items.append(soln_item.strip())



        soln_item = f"""
        \\end {{aligned}}
        $
        """
        soln_items.append(soln_item.strip())


        soln_item = f"""
        Using formula,

        $\\displaystyle

        \\begin {{aligned}}

        {latex(x)}
        & = \\frac {{ - b \\pm \\sqrt{{ b^2 - 4 a c }} }}  {{ 2 a }}
        \\\\ \\\\

        & = \\frac
        {{ - {latex(b)}
            \\pm \\sqrt{{
            \\left({latex(b)}\\right)^2 - 4 \\cdot {latex(a)} \\cdot {latex(c)}
            }}
        }}
        {{ 2 \\cdot {latex(a)}  }}
        \\\\ \\\\

        """
        soln_items.append(soln_item.strip())


        soln_step3 = f"""
        & = \\frac
        {{ {latex(- b)}
            \\pm \\sqrt{{
            {latex( b ** 2 - 4 * a * c  )}
            }}
        }}
        {{ {latex(2 * a)}  }}
        \\\\ \\\\
        """
        soln_items.append(soln_step3.strip())


        soln_step4 = f"""
        & = \\frac
        {{ {latex(- b)}
            \\pm {latex( sp.sqrt(b ** 2 - 4 * a * c ) )}
        }}
        {{ {latex(2 * a)}  }}
        \\\\ \\\\
        """

        if soln_step4.strip() != soln_step3.strip():
            soln_items.append(soln_step4.strip())


        soln_step5 = f"""
        & =

        \\begin {{cases}}

        & \\frac
        {{ {latex(- b)}
            + {latex( sp.sqrt(b ** 2 - 4 * a * c ) )}
        }}
        {{ {latex(2 * a)}  }}
        \\\\ \\\\

        & \\frac
        {{ {latex(- b)}
            - {latex( sp.sqrt(b ** 2 - 4 * a * c ) )}
        }}
        {{ {latex(2 * a)}  }}

        \\end {{cases}}
        \\\\ \\\\
        """
        soln_items.append(soln_step5.strip())

        numr_plus =  - b + sp.sqrt(b ** 2 - 4 * a * c )
        numr_minus =  - b - sp.sqrt(b ** 2 - 4 * a * c )

        soln_step6 = f"""
        & =

        \\begin {{cases}}

        & \\frac
        {{
            {latex(numr_plus)}
        }}
        {{ {latex(2 * a)}  }}
        \\\\ \\\\

        & \\frac
        {{
            {latex(numr_minus)}
        }}
        {{ {latex(2 * a)}  }}

        \\end {{cases}}
        \\\\ \\\\
        """
        if soln_step6.strip() != soln_step5.strip():
            soln_items.append(soln_step6.strip())

        x_plus =  numr_plus / (2 * a)
        x_minus =  numr_minus / (2 * a)

        soln_step7 = f"""
        & =

        \\begin {{cases}}

        & {latex(x_plus)}
        \\\\ \\\\

        & {latex(x_minus)}
        \\\\ \\\\

        \\end {{cases}}
        \\\\ \\\\
        """
        if soln_step7.strip() != soln_step6.strip():
            soln_items.append(soln_step7.strip())



        soln_item = f"""
        \\end {{aligned}}
        $
        """
        soln_items.append(soln_item.strip())


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

        result = {
            'expr': expr,
            'x': x,
            'poly': poly,
            'soln_s': soln_s.strip(),
            }

        return result
        pass


    def solution_to_cubic_eq(self, cubic_eq, x):

        expr_lhs = cubic_eq.lhs
        expr_rhs = cubic_eq.rhs

        soln_items = []

        a = expr_rhs.coeff(x**2)
        b = expr_rhs.coeff(x**1)
        c = expr_rhs.subs(x, 0)


        soln_item = f"""
        We have,

        $\\displaystyle
        {latex(cubic_eq)}
        $

        Expanding the lhs,

        $\\displaystyle
        {latex(cubic_eq.lhs)}
        = {latex(sp.expand(cubic_eq.lhs))}
        $

        """
        soln_items.append(soln_item.strip())


        soln_item = f"""
        $\\displaystyle

        \\begin {{aligned}}

        & {latex(cubic_eq)}
        \\\\ \\\\
        """
        soln_items.append(soln_item.strip())

        eq0 = cubic_eq

        if latex(eq0) != latex(cubic_eq):
            soln_item = f"""
            \\implies
            & {latex(eq0)}
            \\qquad \\text{{ divided both sides by }} {latex(a)}
            \\\\ \\\\
            """
            soln_items.append(soln_item.strip())


        soln_item = f"""
        \\end {{aligned}}
        $
        """
        soln_items.append(soln_item.strip())

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

        answer_s = f"""
        $\\displaystyle
        $
        """

        result = {
            'cubic_eq': cubic_eq,
            'x': x,
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result
        pass


    def is_polynomial_of(self, poly, gen):

        if not isinstance(poly, Poly):
            poly = self.convert_to_polynomial(poly)

        if poly is None:
            txt_msg = f"polynomial is None."
            return [False, txt_msg]

        poly_gens = poly.gens

        if len(poly_gens) == 1:
            if poly.gen == gen:
                txt_msg = f"""
                All terms of the polynomial can be expressed as
                zero or positive integer power of
                $\\displaystyle {latex(gen)}$.
                """
                txt_msg = txt_msg.replace("\n", " ")
                return [True, txt_msg]

        gen_symb = Symbol(latex(gen))

        try:
            mod_poly = Poly(poly.subs(gen, gen_symb))

            if mod_poly is None:
                txt_msg = f""" The polynomial cannot be
                expressed in terms of $\\displaystyle {latex(gen)}$.
                """
                txt_msg = txt_msg.replace("\n", " ")
                return [False, txt_msg]


            if len(mod_poly.gens) == 1:
                if mod_poly.gen == gen_symb:
                    txt_msg = f""" Each term of the polynomial can be
                    expressed with a zero or positive integer power of
                    $\\displaystyle {latex(gen_symb)}$.
                    """
                    txt_msg = txt_msg.replace("\n", " ")
                    return [True, txt_msg]

        except Exception as e:
            sError = f"""{self.__class__.__name__}.{inspect.stack()[0][3]} Error: {e}:
            poly = {poly}
            gen = {gen}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            txt_msg = f""" {e}
            """
            txt_msg = txt_msg.replace("\n", " ")
            return [False, txt_msg]


        for item_gen in poly.gens:
            if gen == item_gen:
                continue

            if str(gen) == str(item_gen):
                continue

            item_gen_mod = item_gen.subs(gen, gen_symb)


            if str(item_gen) == str(item_gen_mod):
                txt_msg = f"""
                $\\displaystyle {latex(item_gen)}$
                cannot be expressed as positive integer power of
                $\\displaystyle {latex(gen_symb)}$
                """
                txt_msg = txt_msg.replace("\n", " ")
                return [False, txt_msg]

            if not isinstance(item_gen_mod, Pow):
                txt_msg = f"""
                $\\displaystyle {latex(item_gen)}$ modified to
                $\\displaystyle {latex(item_gen_mod)}$
                is not a power expression.
                """
                txt_msg = txt_msg.replace("\n", " ")
                return [False, txt_msg]

            item_exp = item_gen_mod.args[1]
            if int(item_exp) != item_exp:
                txt_msg = f"""
                $\\displaystyle {latex(item_gen)}$ modified to
                $\\displaystyle {latex(item_gen_mod)}$
                has non-integer exponent $\\displaystyle {latex(item_exp)}$.
                """
                txt_msg = txt_msg.replace("\n", " ")
                return [False, txt_msg]

            if item_exp < 0:
                txt_msg = f"""
                $\\displaystyle {latex(item_gen)}$ modified to
                $\\displaystyle {latex(item_gen_mod)}$
                has non-positive exponent $\\displaystyle {latex(item_exp)}$.
                """
                txt_msg = txt_msg.replace("\n", " ")
                return [False, txt_msg]


            sOut = f"""
            item_gen = {item_gen}
            item_gen_mod = {item_gen_mod}
            type(item_gen_mod) = {type(item_gen_mod)}
            """
            pass

        txt_msg = f"""Though not a strict multinomial, all terms of the polynomial can be
        expressed as power of $\\displaystyle {latex(gen)}$.
        """
        txt_msg = txt_msg.replace("\n", " ")
        return [True, txt_msg]
        pass



    def whose_polynomial(self, poly):

        if not isinstance(poly, Poly):
            poly = self.convert_to_polynomial(poly)

        if poly is None:
            return None

        for gen in poly.gens:
            is_polynomial, is_polynomial_msg = self.is_polynomial_of(poly, gen)
            if not is_polynomial:
                continue
            return is_polynomial
            pass

        return None
        pass

    def is_polynomial_of_NOT_USED_WORKING(self, poly, x = None):
        if not isinstance(poly, Poly):
            poly = self.convert_to_polynomial(poly)

        if poly is None:
            self._log(f"poly is None")
            return False

        if x is None:
            self._log(f"Input x is None. Returning poly as True for poly = {poly}")
            return True

        if not x in poly.gens:
            self._log(f"{x} is not one of gens of poly = {poly}")
            return False

        try:
            all_terms = poly.all_terms()
            for term_exps, coeff in all_terms:
                for term_exp in term_exps:
                    if not isinstance(term_exp, int):
                        self._log(f"{term_exp} is a non-integer exponent in \n poly = {poly}")
                        return False
                    pass
                pass

            self._log(f"All exponents are integers. Returning poly as True for poly = {poly}")
            return True
        except Exception as e:
            sError = f"""{self.__class__.__name__}.{inspect.stack()[0][3]} Error: {e}:
            poly = {poly}
            """
            self._log(sError)
            #self._log(traceback.format_exc())
            return False
        pass

    def convert_to_polynomial(self, expr, x = None):
        try:
            if x is None:
                return Poly(expr)
            else:
                return Poly(expr, x)
        except:
            return None
        pass

    def get_symbolic_quadratic_polynomial(self, x = None):
        if x is None:
            x = self.x

        a, b, c = symbols("a, b, c")
        expr = a * x**2 + b * x + c
        poly = self.convert_to_polynomial(expr)
        return poly
        pass




    def get_random_quadratic_polynomial(self, x = None):
        if x is None:
            x = self.x

        expr = []
        for i in range(3):
            if i != 2:
                coeff = random.choice([0, 1, -1, random.randint(-9, 9)])
            else:
                coeff = random.choice([1, -1, random.randint(-9, -1), random.randint(1, 9)])

            term = coeff * x ** i
            expr.append(term)
            pass

        expr = Add(*expr)
        poly = self.convert_to_polynomial(expr)
        return poly
        pass


    def get_random_polynomial(self, x = None):
        while True:
            expr = self.get_random_polyvar_expr(x, is_polynomial = True)
            poly = self.convert_to_polynomial(expr)
            if not poly is None:
                return poly
            pass
        pass

    def get_standard_cubic_eq(self, x = None, w = None):
        if x is None:
            x = self.x

        if w is None:
            w = self.w

        a, b = symbols("a, b")
        A, B, C, D = symbols("A, B, C, D")

        lhs = (a * x +  b * w) ** 3
        rhs = A * x**3 + B * x**2 + C * x + D
        cubic_eq = Eq(lhs, rhs)
        return cubic_eq
        pass




    def get_random_polyvar_expr(self, x = None, is_polynomial = True):
        if x is None:
            x = self.x

        poly_type = random.choice([1, 2])

        if poly_type == 1:
            return self._get_random_polyvar_expr_as_Add(x, is_polynomial = is_polynomial)

        return self._get_random_polyvar_expr_as_Mul(x, is_polynomial = is_polynomial)
        pass

    def _get_random_polyvar_expr_as_Add(self, x = None, is_polynomial = True):
        coeffs = np.random.randint(-3, 3, random.randint(2, 5))
        exprs = []
        for i, coeff in enumerate(coeffs):
            if is_polynomial:
                term_exp = i
            else:
                term_exp = self.get_random_exponent(i)
                pass

            term = coeff * x ** term_exp
            exprs.append(term)
            pass
        expr = Add(*exprs)
        return expr
        pass

    def _get_random_polyvar_expr_as_Mul(self, x = None, is_polynomial = True):
        coeffs = np.random.randint(-3, 3, random.randint(2, 5))
        exprs = []
        for i, coeff in enumerate(coeffs):
            if is_polynomial:
                term_exp = 1
            else:
                term_exp = self.get_random_exponent(i)
                pass

            term = x ** term_exp - coeff
            exprs.append(term)
            pass

        expr = expand(Mul(*exprs))
        return expr
        pass


    def get_random_exponent(self, i):
        exponent_type = random.choice(["int", "neg_int", "inv_int",
                                       "sqrt_int", "rand_int"])

        if exponent_type == "int":
            return i

        if exponent_type == "neg_int":
            if i == 0:
                i = 1
            return -i

        if exponent_type == "inv_int":
            if i < 2:
                i = 2
            return Rational(1, i)

        if exponent_type == "sqrt_int":
            if i < 2:
                i = 2
            return sqrt(i)

        if exponent_type == "rand_int":
            return random.randint(1, 3)

        pass


    def get_polynomial_factors(self, poly_expr):
        if not hasattr(poly_expr, 'factor_list'):
            return poly_expr
            pass

        coeff, temp_factors = poly_expr.factor_list()

        if coeff != 1:
            factors = [coeff]
        else:
            factors = []

        for idx, temp_factor in enumerate(temp_factors):
            if isinstance(temp_factor, tuple):
                if len(temp_factor) == 3:
                    t_coeff, t_expr, t_exp = temp_factor
                else:
                    t_coeff = 1
                    t_expr, t_exp = temp_factor
                    pass

                if isinstance(t_expr, Poly):
                    t_expr = t_expr.expr

                factors.append(t_coeff * t_expr ** t_exp)
                continue

            factors.append(temp_factor)
            pass

        factors = [self._get_symb_with_bracket(factor, pow_bracket = False)
                   for factor in factors]
        factors = Mul(*factors)
        return factors
        pass




    def _get_symb_with_bracket(self, expr, **kwargs):
        from xv.util import convert_to_symb
        return convert_to_symb(expr, **kwargs)

    def _get_expr_latex(self, expr):
        if not (isinstance(expr, list) or isinstance(expr, tuple)):
            if hasattr(expr, 'expr'):
                return self._get_symb_with_bracket(expr.expr)
            else:
                return self._get_symb_with_bracket(expr)

        expr_item_latexes = 1
        for expr_item in expr:
            expr_item_latex = self._get_expr_latex(expr_item)
            if expr_item_latex == 1:
                continue
            expr_item_latexes = expr_item_latexes * ueval(expr_item_latex)
            pass

        return expr_item_latexes
        pass


    def get_random_function_for_series(self, maclaurin = False):
        exp_val = random.randint(1, 3)
        half = Rational(1, 2)
        x = self.x

        data = [
            [sin(x), 0],
            [cos(x), 0],
            [tan(x), 0],
            [asin(x), 0],
            [acos(x), 0],
            [atan(x), 0],
            [sinh(x), 0],
            [cosh(x), 0],
            [tanh(x), 0],
            [exp(x), 0],
            [1/(1 - x)**exp_val, 0],
            [(1 + x)**(1/2), 0],
            [(1 + x)**(-1/2), 0],
            [log(1 + x), 1],
            [1/(1 - x)**exp_val, 0],
            [(1 + x)**half, 0],
            [(1 + x)**(-half), 0],
            [1/sqrt(1 - x**2), 1],
            ]

        if maclaurin:
            data = [item for item in data
                    if item[1] == 0]

        func, a = random.choice(data)

        return func, a
        pass


    def compute_standard_taylor_series(self, x, a, n):
        x_val = a

        eq = self.get_polynomial_equation(x = x, a = a)
        derivative_series = self.get_derivative_series(eq,
                                x = x, n = n)

        derivative_series_str = self.get_derivative_series_str(
                                        derivative_series,
                                        line_prefix = "")

        derivative_series_subs = self.get_derivative_series(eq,
                                x = x, n = n, x_val = x_val,
                                include_eq = True)

        derivative_series_subs_str = self.get_derivative_series_str(
                            derivative_series_subs,
                            cdots = False,
                            line_prefix = "")


        const_solns = self.get_const_solns(derivative_series_subs)
        const_solns_str = self.get_const_solns_str(const_solns)

        rewitten_eq = self.rewite_eq(eq, const_solns)
        rewitten_eq_latex = self.rewite_eq_latex(eq, const_solns)


        result = {
            'eq': eq,
            'eq_latex': self.readable_latex(eq),

            'const_solns_str': const_solns_str,

            'derivative_series': derivative_series,
            'derivative_series_str': derivative_series_str,

            'derivative_series_subs_str': derivative_series_subs_str,

            'rewitten_eq': rewitten_eq,
            'rewitten_eq_latex': rewitten_eq_latex,
            }

        return result
        pass


    def compute_taylor_series_for_a_func(self, func, x, a, n, x_val):
        std_series_equation = self.get_std_taylor_equation(x = x, a = a)

        eq_applied = Eq(std_series_equation.lhs, func)

        derivative_series = self.get_derivative_series(eq_applied,
                                x = x, n = n)

        derivative_series_str = self.get_derivative_series_str(
                                        derivative_series,
                                        line_prefix = "",
                                        cdots = False)

        derivative_series_subs = self.get_derivative_series(eq_applied,
                                x = x, n = n, x_val = x_val,
                                include_eq = True)

        derivative_series_subs_str = self.get_derivative_series_str(
                            derivative_series_subs,
                            cdots = False,
                            line_prefix = "")


        coeff_dict = {row_eq.lhs: row_eq.rhs
                     for row_eq in derivative_series_subs}

        taylor_equation = self.get_taylor_equation(eq_applied.rhs,
                                                 x = x, a = a)

        rewitten_eq = self.rewite_eq(taylor_equation, coeff_dict)

        rewitten_eq_latex = self.rewite_eq_latex(taylor_equation, coeff_dict)

        rewitten_eq_final_latex = self.rewite_eq_latex(
                    taylor_equation, coeff_dict, zero_terms = False)

        answer = Eq(taylor_equation.lhs,
                    taylor_equation.lhs.series(x, a, 7))

        result = {
            'eq_applied': eq_applied,
            'eq_applied_latex': self.readable_latex(eq_applied),

            'func': eq_applied.rhs,
            'func_latex': self.readable_latex(eq_applied.rhs),

            'derivative_series': derivative_series,
            'derivative_series_str': derivative_series_str,

            'derivative_series_subs_str': derivative_series_subs_str,

            'std_series_equation': std_series_equation,
            'std_series_equation_latex': self.readable_latex(std_series_equation),

            'rewitten_eq': rewitten_eq,

            'rewitten_eq_latex': rewitten_eq_latex,
            'rewitten_eq_final_latex': rewitten_eq_final_latex,

            'answer': answer,
            }

        return result
        pass

    def plot_taylor_expr(self,
                         expr,
                         taylor_expr,
                         x,
                         x_val,
                         figsize = (10,6),
                         use_cache = True
                         ):

        try:
            title = f'Plotting of ${latex(expr)}$ with Taylor series'
            verbose = self.verbose

            self.cacheUtil = CacheUtil(use_cache = use_cache, verbose = verbose)

            cache_file_path, exists_cached_file = self.cacheUtil.get_cached_path(
                    self.__class__, inspect.stack()[0][3],
                    expr,
                    taylor_expr,
                     x,
                     x_val,
                    figsize,
                    title,
                    file_prefix = f"{self.__class__.__name__}_{inspect.stack()[0][3]}_"
                    )

            if use_cache and exists_cached_file:
                return self.cacheUtil.load_file(cache_file_path)

            plt.ioff()
            plt.figure(figsize = figsize)

            ax = plt.gca()

            x_val = float(x_val)

            ax.plot(x_val, expr.subs({x: x_val}),
                    marker = '*', color = 'green', markersize = 10)

            xs_temp = np.linspace(x_val - 1, x_val + 1, 21)

            xs = []
            ys_taylor = []
            ys = []

            for item in xs_temp:
                y_taylor = taylor_expr.subs({x: item})
                y = expr.subs({x: item})
                try:
                    ys_taylor.append(y_taylor.n())
                    ys.append(y.n())
                    xs.append(item)
                    pass
                except:
                    pass
                pass

            ax.plot(xs, ys,
                    color = 'blue',
                    label = 'actual value')
            ax.plot(xs, ys_taylor,
                    color = 'red',
                    label = 'Taylor series value')

            ax.set_title(title)
            plt.legend()

            svgHelper = SvgHelper()
            svg_str = svgHelper.plt_to_svg()

            if not svg_str:
                self._log(f"Error: svg_str could not be initialized")
            else:
                self.cacheUtil.save_file(svg_str, cache_file_path)

            #plt.figure().clear()
            plt.clf()
            plt.close()

            return svg_str
        except Exception as e:
            sError = f"""{self.__class__.__name__}.{inspect.stack()[0][3]} Error: {e}:
            expr = {expr}
            x = {x}
            x_val = {x_val}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            return None
        pass


    def readable_latex(self, expr, x = None, a = None):
        if x is None:
            x = self.x
        if a is None:
            a = self.a

        if a == 0:
            return latex(expr)

        x_a = Symbol(f"({latex(x)} - {latex(a)})")
        expr_modified = expr.subs(x - a, x_a)
        return latex(expr_modified)
        pass

    def get_function(self, x = None, a = 0):
        if x is None:
            x = self.x

        f = Function('f')
        #f = f(x - a)
        f = f(x)
        return f

    def get_const_symbols_latex(self, m, n):
        const_symbols = self.get_const_symbols(m, n)
        const_symbols_latex = [latex(const_symbol)
                             for const_symbol in const_symbols]
        const_symbols_latex = " , \\; ".join(const_symbols_latex)
        return const_symbols_latex

    def get_const_symbols(self, m, n):
        const_symbols = []
        for i in range(m, n):
            const_symbols.append(self.get_const_symbol(i))
            pass
        return const_symbols
        pass

    def get_const_symbol(self, n):
        return Symbol(f"c_{{{n}}}", real = True)
        pass

    def get_maclaurin_equation(self, func, x = None, n = None):
        eq = self.get_std_maclaurin_equation(x = x, n = n)
        return Eq(func, eq.rhs)
        pass

    def get_taylor_equation(self, func, x = None, a = None, n = None):
        eq = self.get_std_taylor_equation(x = x, a = a, n = n)
        return Eq(func, eq.rhs)
        pass


    def get_std_maclaurin_equation(self, x = None, n = None):
        return self.get_std_taylor_equation(x = x, a = 0, n = n)
        pass


    def get_std_taylor_equation(self, x = None, a = None, n = None):
        if n is None:
            n = self._terms

        f = self.get_function(x, a)
        terms = self.get_taylor_terms(x = x, a = a, n = n)
        eq = Eq(f, sum(terms))
        return eq
        pass

    def get_taylor_terms(self, x = None, a = None, n = None):
        if x is None:
            x = self.x

        if a is None:
            a = self.a

        if n is None:
            n = self._terms

        terms = [self._get_taylor_term(x, a, i)
                 for i in range(n)]
        return terms
        pass

    def _get_taylor_term(self, x, a, n):
        fprime = self._get_function_prime_format(a, n)
        k =  self._get_factorial_inverse_latex(n) * fprime

        if n == 0:
            term = ueval(k)
        else:
            term = ueval(k) * ueval((x - a)**n)

        return term


    def get_polynomial_equation(self, x = None, a = 0, n = None, evaluate = True):
        if n is None:
            n = self._terms

        f = self.get_function(x, a)
        terms = self.get_polynomial_terms(x = x, a = a, n = n, evaluate = evaluate)
        eq = Eq(f, sum(terms))
        return eq
        pass

    def get_polynomial_terms(self, x = None, a = 0, n = None, evaluate = True):
        if x is None:
            x = self.x

        if n is None:
            n = self._terms

        terms = [self._get_polynomial_term(x, a, i, evaluate = evaluate)
                 for i in range(n)]

        return terms
        pass

    def _get_polynomial_term(self, x, a, n, evaluate = True):
        k = self.get_const_symbol(n)

        if not evaluate:
            k = self._get_symb_with_bracket(k, pow_bracket = True)
            #x_a = self._get_symb_with_bracket(x-a, pow_bracket = True)

            #term = k * x_a ** n
            term = Symbol(f"{latex(k)} \\; \\left( {latex(x-a)} \\right)^{{{n}}}")

            return term

        if n == 0:
            term = ueval(k)
            return term

        term = ueval(k) * ueval((x - a)**n)
        return term

    def get_derivative_series(self, eq,
                              x = None, x_val = None, n = None,
                              include_eq = False):
        if x is None:
            x = self.x

        if n is None:
            n = self._terms

        rows = []
        if include_eq:
            if not x_val is None:
                lhs = simplify(eq.lhs.subs({x: x_val}))
                rhs = simplify(eq.rhs.subs({x: x_val}))
                eq_subs = Eq(lhs, rhs)
                rows.append(eq_subs)
            else:
                rows.append(eq)

        prev_eq = eq
        #for i in range(n + 1):
        for i in range(n):

            rhs = self._get_rhs_derivative(prev_eq.rhs, x)

            #lhs = Symbol(f"""f^{{{i+1}}}({latex(x)})""")
            lhs = self._get_function_prime_format(x, i+1)
            eq_diff = Eq(lhs, rhs)

            prev_eq = eq_diff.copy()

            if not x_val is None:
                #lhs = Symbol(f"""f^{{{i+1}}}({latex(x_val)})""")
                lhs = self._get_function_prime_format(x_val, i+1)
                rhs = simplify(rhs.subs({x: x_val}))
                eq_diff = Eq(lhs, rhs)

            rows.append(eq_diff)
            pass
        return rows
        pass


    def _get_function_prime_format(self, x, n):
        if n == 0:
            f = Function("f")
        else:
            f = Function(f"""f^{{{n}}}""")

        f = f(x)
        return f
        pass

    def _get_rhs_derivative(self, rhs, x):
        if not isinstance(rhs, Add):
            return diff(rhs, x)

        terms = [simplify(term) for term in rhs.args
                 if simplify(term) != 0]

        terms = [factor(diff(term, x))
               for term in terms]

        rhs = [ueval(term)
               for term in terms
               if term != 0]

        rhs = sum(rhs)
        return rhs
        pass

    def get_const_solns(self, derivative_series):
        consts_solns = {}

        consts = [self.get_const_symbol(i)
                  for i in range(self._terms)]

        for i, const in enumerate(consts):
            for j, eq_der in enumerate(derivative_series):
                if const in eq_der.free_symbols:
                    solns = solve(eq_der, const)
                    solns = [self._subs_factorials(soln, j)
                             for i, soln in enumerate(solns)]
                    consts_solns.update({const: solns})
                    break
                pass
            pass

        return consts_solns
        pass


    def rewite_eq(self, eq, val_dict, zero_terms = True):
        rewitten_eq, rewitten_eq_latex = self._rewite_eq(
                        eq, val_dict, zero_terms = zero_terms)
        return rewitten_eq
        pass



    def rewite_eq_latex(self, eq, val_dict, zero_terms = True):
        rewitten_eq, rewitten_eq_latex = self._rewite_eq(
                        eq, val_dict, zero_terms = zero_terms)
        return rewitten_eq_latex
        pass


    def _rewite_eq(self, eq, val_dict, zero_terms = True):
        mod_val_dict = {}
        for k, v in val_dict.items():
            if isinstance(v, list):
                v = v[0]
            mod_val_dict[k] = v
            pass

        if not isinstance(eq.rhs, Add):
            eq = eq.subs(mod_val_dict)
            return eq

        rhs_args = sorted(eq.rhs.args,
                          key = lambda term: self._sort_index_val_dict_key(term, val_dict),
                          reverse = False)

        rhs = []
        rhs_latex = []
        for term in rhs_args:

            #term = term.subs(mod_val_dict)
            term = simplify(term.subs(mod_val_dict))

            if not zero_terms:
                if term == 0:
                    continue

            rhs.append(ueval(term))

            rhs_latex.append(f"{self.readable_latex(term)}")

            pass

        rewitten_eq = Eq(eq.lhs, sum(rhs))

        rhs_latex = " + ".join(rhs_latex)
        rewitten_eq_latex = f"{self.readable_latex(eq.lhs)} = {rhs_latex}"

        return rewitten_eq, rewitten_eq_latex
        pass
        #rewite_eq_latex

    def _sort_index_val_dict_key(self, term, val_dict):
        ks = list(val_dict.keys())

        ks = sorted(ks,
                      key = lambda k: str(k),
                      reverse = False)

        for i, k in enumerate(ks):
            #print(f"{k}: {i}: {term} : {term.free_symbols}")
            #if k in term.free_symbols:
            if latex(k) in latex(term):
                return i
        return 99999

    def _subs_factorials(self, expr, i):
        expr_mod = expr.subs(1/factorial(i),
                             self._get_factorial_inverse_latex(i))
        return expr_mod
        pass

    def _get_factorial_inverse_latex(self, n):
        return Symbol(f"\\frac{{1}}{{{n}!}}")

    def get_const_solns_str(self, const_solns,
                                  line_break = "\n\n"):

        terms = [(k, ",".join([f"{latex(v)}" for v in vs]))
                 for k, vs in const_solns.items()]

        derivative_series_str = [
            f"$\\displaystyle {latex(k)} = {v}$"
            for k, v in terms]

        derivative_series_str = line_break.join(derivative_series_str)
        return derivative_series_str
        pass


    def get_derivative_series_str(self, derivative_series,
                                  line_break = "\n\n",
                                  line_prefix = "\\implies",
                                  cdots = True):
        if cdots:
            cdots_str = f"\\cdots"
        else:
            cdots_str = ""

        derivative_series_str = [
            f"$\\displaystyle {line_prefix} {self.readable_latex(eq)}{cdots_str}$"
            for eq in derivative_series]


        derivative_series_str = line_break.join(derivative_series_str)
        return derivative_series_str
        pass

    def get_derivative(self, polynomial, x = None, x_val = None):
        if x is None:
            x = self.x

        polynomial_d = []
        for t in polynomial:
            t_d = diff(t,x)
            if t_d == 0:
                continue
            if not x_val is None:
                polynomial_d.append(t_d.subs(x, x_val))
                pass
            else:
                polynomial_d.append(factor(t_d))

        return polynomial_d
        pass

    def get_polynomial_str(self, polynomial):
        polynomial_str = [f"{latex(term)}" for term in polynomial]
        polynomial_str = " + ".join(polynomial_str) + " + ... "
        return polynomial_str
        pass

    def get_pol_arr(self, polynomial_type = None):
        if polynomial_type is None:
            polynomial_type = ""
        else:
            polynomial_type = polynomial_type.replace(" ", "").lower()

        if polynomial_type == "taylor":
            A = self._ds_taylor
            B = self._ax_is
        elif polynomial_type == "maclaurin":
            A = self._ds_maclaurin
            B = self._x_is
        elif polynomial_type == "x-a":
            A = self._cs
            B = self._ax_is
        else:
            A = self._cs
            B = self._x_is

        pol_arr = np.array([
            f"{A[i]}\,{B[i]}"
            for i in range(self._terms)
            ])
        return pol_arr
        pass


    def polynomial_subs(self, polynomial, x_val, x = None):
        if x is None:
            x = self.x

        polynomial_s = []
        for t in polynomial:
            t_s = t.subs(x, x_val)
            if t_s == 0:
                continue
            polynomial_s.append(t_s)

        return polynomial_s
        pass



    def get_polynomial_expr_str(self, polynomial, x = None):
        polynomial_str = [
            f"{a}".replace("**", "^").replace(r"*", r"\,").replace("-a + x", "x - a")
                          for i, a in enumerate(polynomial)]
        polynomial_str = " + ".join(polynomial_str) + " + ..."
        return polynomial_str
        pass

    def get_series_as_str(self, series):
        if len(series) == 1:
            return str(series[0]).replace(r"*", r"\,")
            pass
        series_str = " + ".join(str(t).replace(r"*", r"\,") for t in series) + " + ..."
        return series_str
        pass

    def get_random_function(self, x = None, x_val = 0):
        from sympy import ( exp,
                            sin, cos, tan,
                            cosh, sinh,
                            asin, acos, atan,
                            ln
                            )

        func = random.choice([
            exp(x),
            sin(x),
            cos(x),
            tan(x),
            asin(x),
            acos(x),
            atan(x),
            sinh(x),
            cosh(x),
            ln(1 + x),
            1 / (1 - x),
            1 / (1 + x)** Rational(1, 2)
            ])

        #print(f"func = {func}")
        return func
        pass

    def get_factorial_form(self, n):

        #print(f"n = {n}  {type(n)}")

        if not (isinstance(n, One) or isinstance(n, Integer)):
            return n
        i = 1
        while True:
            i= i +1
            if factorial(i) == n:
                return f"{i}!"

            if factorial(i) > n:
                break
            pass

        return n
        pass


    def _init_data(self):
        self._init_constants()
        self._init_maclaurin()
        self._init_taylor()
        self._init_polynomial_of_x()
        self._init_polynomial_of_xa()
        pass


    def _set_terms(self, n):
        self.x = symbols('x')
        self.k = symbols('a', constant = True)
        self._terms = n
        pass

    def _init_constants(self):
        terms = [Symbol(f"c_{i}") for i in range(self._terms)]
        self._cs = np.array(terms)

    def _init_maclaurin(self):
        terms = [latex_fraction(Symbol(f"f^{i}(0)"), f"{i}!")
                 for i in range(self._terms)]
        self._ds_maclaurin = np.array(terms)

    def _init_taylor(self):
        terms = [latex_fraction(Symbol(f"f^{i}({self.k})"), f"{i}!")
                 for i in range(self._terms)]
        self._ds_taylor = np.array(terms)

    def _init_polynomial_of_x(self):
        self._polynomial = [symbols(f"c_{i}") * (self.x ** i)
                            for i in range(self._terms)]
        xVarStr2 = f"{self.x}"
        self._x_is = np.array([f"{{{xVarStr2}}}^{{{i}}}"
                               for i in range(self._terms)])
        pass

    def _init_polynomial_of_xa(self):
        self._polynomial_xa = [symbols(f"c_{i}") * ((self.x - self.k) ** i)
                               for i in range(self._terms)]

        xVarStr1 = f"({self.x} - {self.k})"
        self._ax_is = np.array([f"{{{xVarStr1}}}^{{{i}}}"
                                for i in range(self._terms)])

        pass


    def _log_DELETED(self, sOut, to_print = None):
        if to_print is None:
            to_print = self.verbose

        if to_print:
            print(sOut)



    pass
#class PolynomialHelper:
