'''
Created on 12-Jul-2021

@author: bthakur

'''
import inspect
import traceback

from __future__ import annotations

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

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

from sympy import UnevaluatedExpr as ueval

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

from ._derivative_solver import DerivativeSolver

# _differential_equation_helper
# DifferentialEquationHelper

from xv.util import UtilClass

class DifferentialEquationHelper(UtilClass):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.a = symbols('a', constant = True)
        self.b = symbols('b', constant = True)
        self.n = symbols('n', constant = True)

        self.x = symbols('x')
        self.y = Function('y')(self.x)
        self.z = symbols('z')
        pass

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


    def get_random_separable_functions(self, keyword = None):
        ltx = self._modified_latex

        data = self.get_separable_functions_data(keyword = keyword)

        x = data['x']
        y = data['y']
        diff_eqs = data['diff_eqs']
        dy_dx = Derivative(y, x)

        diff_eq = random.choice(diff_eqs)
        dy_dx_sol = solve(diff_eq, dy_dx)

        if len(dy_dx_sol) != 1:
            sError = f"""
            Error: Issue with dy_dx_sol = {dy_dx_sol}
            diff_eq = {diff_eq}
            dy_dx = {dy_dx}
            x = {x}
            y = {y}
            """
            print(sError)
            raise Exception(sError)
            pass

        dy_dx_sol = dy_dx_sol[0]
        result = self._solve_diff_eq_with_separable_xy_function(diff_eq, y, x, dy_dx_sol)

        problem_s = f"""
        Solve: $\\displaystyle {latex(diff_eq)} $
        """
        result['problem_s'] = problem_s.strip()
        result.update(data)
        return result
        pass

    def _solve_diff_eq_with_separable_xy_function(self, diff_eq, y, x, dy_dx_sol):
        dy = Symbol(f"d{latex(y)}")
        dx = Symbol(f"d{latex(x)}")

        dy_dx = Derivative(y, x)
        dy_dx_eq0 = Eq(dy_dx, dy_dx_sol)

        soln_items = []

        soln_item = f"""
        $\\displaystyle {latex(diff_eq)} $

        $\\displaystyle \\implies {latex(dy_dx_eq0)} $

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

        fxy, fy, fx, fcoeff = self._get_args_factors(dy_dx_sol, x, y)

        if str(fxy) != '1':
            soln_item =f"""
            The functions are not separable:
            $\\displaystyle fxy = {latex(fxy)}$
            $\\displaystyle fy = {latex(fy)}$
            $\\displaystyle fx = {latex(fx)}$
            $\\displaystyle fcoeff = {latex(fcoeff)}$
            """
            soln_items.append(soln_item.strip())

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

            result = {
                'x': x,
                'y': y,
                'dy_dx': dy_dx,
                'diff_eq': diff_eq,
                'soln_s': soln_s.strip(),
                'answer_s': "Not able to solve.",
                }
            return result
            pass

        lhs = 1
        lhs_symb = 1
        if fy != 1:
            lhs_symb = Symbol(f"\\left( {latex(1 / fy)} \\right)")
            lhs = ueval(1 / fy)

        rhs = 1
        rhs_symb = 1
        if (fx != 1 and fcoeff != 1):
            rhs_symb = Symbol(f"{latex(fcoeff)} \\; \\left( {latex(fx)} \\right)")
            rhs = ueval(fcoeff) * ueval(fx)
        elif fx != 1:
            rhs_symb = Symbol(f"\\left( {latex(fx)} \\right)")
            rhs = ueval(fx)
        else:
            rhs_symb = Symbol(f"\\left( {latex(fcoeff)} \\right)")
            rhs = ueval(fcoeff)


        dy_dx_eq1 = Eq(lhs_symb * ueval(dy_dx), rhs_symb)
        dy_dx_eq2 = Eq(lhs_symb * ueval(dy), rhs_symb * ueval(dx))

        y_eq = Eq(Integral(lhs, y), Integral(rhs, x))
        y_eq_doit = y_eq.doit()

        y_val = solve(y_eq_doit, y)
        if len(y_val) == 1:
            y_val = y_val[0]


        soln_item = f"""
        dy_dx_eq1 = $\\displaystyle \\implies {latex(dy_dx_eq1)} $

        dy_dx_eq2 = $\\displaystyle \\implies {latex(dy_dx_eq2)} $

        y_eq = $\\displaystyle \\implies {latex(y_eq)} $

        y_eq_doit = $\\displaystyle \\implies {latex(y_eq_doit)} $

        $\\displaystyle \\implies {latex(y)} = {latex(y_val)} $

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

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

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

        result = {
            'x': x,
            'y': y,
            'dy_dx': dy_dx,
            'diff_eq': diff_eq,
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),

            }

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

        return result
        pass

    def _get_args_factors(self, expr, x, y):
        expr_factors = factor(expr)

        y_symb = Symbol('y')

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

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

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

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

            fcoeff = expr_subs
            return fxy, fy, fx, fcoeff
            pass

        fx_args = [1]
        fy_args = [1]
        fxy_args = [1]
        fcoeff_args = [1]
        for arg in expr_factors.args:
            arg_subs = arg.subs(y, y_symb)

            if (y_symb in arg_subs.free_symbols and
                x in arg_subs.free_symbols):
                fxy_args.append(arg)
                continue

            if y_symb in arg_subs.free_symbols:
                fy_args.append(arg)
                continue

            if x in arg_subs.free_symbols:
                fx_args.append(arg)
                continue

            fcoeff_args.append(arg)
            pass
        fcoeff = Mul(*fcoeff_args)
        fx = Mul(*fx_args)
        fy = Mul(*fy_args)
        fxy = Mul(*fxy_args)
        return fxy, fy, fx, fcoeff
        pass


    def get_random_transform_to_first_order_non_intutive(self, keyword = None):
        ltx = self._modified_latex

        data = self.get_first_order_expr_data(keyword = keyword)
        expr = data['expr']
        z = data['z']
        zexpr = data['zexpr']
        dz_dx = data['dz_dx']
        dzexpr_dx_ueval_ltx = data['dzexpr_dx_ueval_ltx']
        dzexpr_dx = data['dzexpr_dx']
        dzexpr = data['dzexpr']

        problem_s = f"""
        Transform the following expr into first order differential form:

        $\\displaystyle
        {ltx(expr, mul_symbol='dot')}
        $

        """


        soln_items = []

        soln_item = f"""
        Let $\\displaystyle
        {ltx(z)} =
        {ltx(zexpr, mul_symbol='dot')}
        $

        $\\displaystyle
        {ltx(dz_dx, mul_symbol='dot')}
        $

        $\\displaystyle
        = {dzexpr_dx_ueval_ltx}
        $

        $\\displaystyle
        = {ltx(dzexpr_dx, mul_symbol='dot')}
        $

        $\\displaystyle
        = {ltx(dzexpr, mul_symbol='dot')}
        $

        $\\displaystyle
        \\implies {ltx(dzexpr, mul_symbol='dot')}
         = {ltx(dz_dx, mul_symbol='dot')}
         \\qquad \\qquad \\text{{where }}
        {ltx(z)} = {ltx(zexpr, mul_symbol='dot')}
        $


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


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


        answer_s = f"""
        $\\displaystyle
        {ltx(dz_dx, mul_symbol='dot')}
         \\qquad \\text{{where }}
        {ltx(z)} = {ltx(zexpr, mul_symbol='dot')}
        $

        """


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

        result.update(data)
        return result
        pass

    def get_random_transform_to_first_order_intutive(self, keyword = None):
        ltx = self._modified_latex

        data = self.get_first_order_expr_data(keyword = keyword)
        expr = data['expr']
        x = data['x']
        y = data['y']
        z = data['z']
        dz = data['dz']
        zexpr = data['zexpr']
        dz_dx = data['dz_dx']

        dzexpr_dx_ueval = data['dzexpr_dx_ueval']
        dzexpr_dx_ueval_ltx = data['dzexpr_dx_ueval_ltx']
        dzexpr_dx = data['dzexpr_dx']
        dzexpr = data['dzexpr']
        dx = data['dx']
        dx_dx_ltx = latex_fraction(ltx(dx), ltx(dx))

        dzexpr_dx_ltx = latex_fraction(ltx(dzexpr, mul_symbol='dot'), ltx(dx))


        problem_s = f"""
        Transform the following expr into first order differential form:

        $\\displaystyle
        {ltx(expr, mul_symbol='dot')}
        $

        """

        str0 = ltx(dzexpr_dx, mul_symbol='dot')

        soln_items = []

        soln_item = f"""
        $\\displaystyle
        {ltx(dzexpr, mul_symbol='dot')}
        $

        $\\displaystyle
         = \\left( {ltx(dzexpr, mul_symbol='dot')} \\right) {dx_dx_ltx}
        $

        $\\displaystyle
         = \\left( {dzexpr_dx_ltx} \\right) {ltx(dx)}
        $

        $\\displaystyle
         = \\left( {str0} \\right) {ltx(dx)}
        $
        """
        soln_items.append(soln_item.strip())


        #add reverse detailed solution
        ##############################
        reverse_soln_stmts = self._get_reverse_soln_stmts(data)
        if reverse_soln_stmts:
            soln_items.extend(reverse_soln_stmts)


        soln_item = f"""
        $\\displaystyle
         = {{d}} \\left( {ltx(zexpr, mul_symbol='dot')}\\right)
        $

        $\\displaystyle
        = {ltx(dz)}
         \\qquad \\qquad \\text{{where }}
        {ltx(z)} = {ltx(zexpr, mul_symbol='dot')}
        $
        """
        soln_items.append(soln_item.strip())


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


        answer_s = f"""
        $\\displaystyle
        {ltx(dz)}
         \\qquad \\qquad \\text{{where }}
        {ltx(z)} = {ltx(zexpr, mul_symbol='dot')}
        $
        """


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

        result.update(data)

        return result
        pass


    def get_random_linear_first_order(self, keyword = None):
        ltx = latex #self._modified_latex

        data = self.get_linear_first_order_data(keyword = keyword)
        x = data['x']
        x0 = data['x0']
        dx = Symbol(f"d{latex(x)}")
        y = data['y']
        y0 = data['y0']
        eq = data['eq']
        std_eq = data['std_eq']
        p = data['p']
        g = data['g']
        expr_dict = data['expr_dict']
        boundary_dict = data['boundary_dict']

        comb_dict = expr_dict.copy()
        comb_dict.update(boundary_dict)

        expr_dict, expr_dict_str = self._get_dict_and_str(expr_dict)
        boundary_dict, boundary_dict_str = self._get_dict_and_str(boundary_dict)



        soln_items = []

        soln_item = f"""
        <h3>Part 1: general solution</h3>
        $\\displaystyle
        {ltx(eq)}
        $

        The equation is a form of the linear first-order differential equation.

        $$\\displaystyle
        {ltx(std_eq)} \\quad \\text{{Where}} \\left \\{{

        \\begin{{aligned}}

        & {ltx(p)} = {ltx(expr_dict[p])}  \\\\

        & {ltx(g)} = {ltx(expr_dict[g])}  \\\\

        \\end{{aligned}}

        \\right .
        $$

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


        mu = Function(f'\\mu')(x)
        dmu = Symbol(f"d{latex(mu)}")

        mu0 = Symbol(f'\\mu_0')
        intg_wrt_x = (x, x0, x)
        intg_wrt_mu = (mu, mu0, mu)



        mu_p_eq = Eq(mu * p, Derivative(mu, x))

        std_eq1 = Eq(expand(mu * std_eq.lhs) , mu * std_eq.rhs)

        std_eq2 = std_eq1.subs(mu_p_eq.lhs, mu_p_eq.rhs)

        std_eq3 = Eq(Derivative(mu * y, x), std_eq2.rhs)

        mu_y = Symbol(f"{latex(mu * y)}")
        d_mu_y = Symbol(f"d\\left( {latex(mu_y)}\\right)")

        std_eq4 = Eq(d_mu_y, ueval(std_eq2.rhs) * ueval(dx))

        std_eq5 = Eq(Integral(1, (mu_y , mu0 * y0, mu_y)),
                     Integral(std_eq3.rhs, intg_wrt_x))

        std_eq6 = std_eq5.doit()

        intg_term_in_soln = std_eq6.rhs

        y_soln = solve(std_eq6, y)

        y_soln_num = ueval(mu0 * y0) + ueval(std_eq6.rhs)
        y_soln_den = mu


        y_soln_eq = Eq(y, ueval(1 / y_soln_den) * y_soln_num)

        soln_item = f"""
        Let us multiply it with a function $\\displaystyle
        {ltx(mu)}
        $

        $\\displaystyle
        {ltx(std_eq1)}
        $

        Let the function $\\displaystyle {ltx(mu)} $ be such that

        $\\displaystyle
        {ltx(mu_p_eq)} \\qquad \\qquad \\cdots (i)
        $

        $\\displaystyle
        \\implies {ltx(std_eq2)}
        $

        $\\displaystyle
        \\implies {ltx(std_eq3)}
        $

        $\\displaystyle
        \\implies {ltx(std_eq4)}
        $

        $\\displaystyle
        \\implies {ltx(std_eq5)}
        $

        $\\displaystyle
        \\implies {ltx(std_eq6)}
        $

        $\\displaystyle
        \\implies {ltx(y_soln_eq)}  \\qquad \\qquad \\cdots (ii)
        $

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

        one_mu_symb = Symbol(latex(1/mu))

        mu_p_eq2 = Eq(ueval(p) * ueval(dx) , ueval(one_mu_symb) * ueval(dmu))

        mu_p_eq3 = Eq(Integral(p, intg_wrt_x) , Integral(1 / mu, intg_wrt_mu))

        mu_p_eq4 = mu_p_eq3.doit()

        mu_soln = solve(mu_p_eq4, mu)[0]

        mu_p_eq5 = Eq(mu, mu_soln)

        soln_item = f"""
        From (i):

        $\\displaystyle
        {ltx(mu_p_eq)}
        $

        $\\displaystyle
        \\implies {ltx(mu_p_eq2)}
        $

        $\\displaystyle
        \\implies {ltx(mu_p_eq3)}
        $

        $\\displaystyle
        \\implies {ltx(mu_p_eq4)}
        $

        $\\displaystyle
        \\implies {ltx(mu_p_eq5)}
        $


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


        soln_item = f"""
        <h3>Part 2: summary of the process</h3>
        Hence, while solving the linear first-order differential equation:

        $\\displaystyle
        {ltx(std_eq)}
        $

        We have to find:

        $\\displaystyle
        {ltx(mu_p_eq5)}
        $

        And pass the value of $\\displaystyle {ltx(mu)} $ in (ii)

        $\\displaystyle
        {ltx(y_soln_eq)}
        $

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


        mu_exponent = Integral(p, intg_wrt_x)
        mu_expr = mu0 * exp(mu_exponent)


        soln_item = f"""
        <h3>Part 3: substitute values</h3>
        The given equation is:
        $\\displaystyle
        {ltx(eq)}
        $

        Compare it with the standard linear first-order differential equation.

        $$\\displaystyle
        {ltx(std_eq)} \\quad \\text{{Where}} \\left \\{{

        \\begin{{aligned}}

        & {ltx(p)} = {ltx(expr_dict[p])}  \\\\

        & {ltx(g)} = {ltx(expr_dict[g])}  \\\\

        \\end{{aligned}}

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



        dct, dct_str = self._get_dict_and_str(comb_dict, keys = [p, x0, y0])
        mu_eq0 = mu_p_eq5.subs(dct)
        mu_eq1 = mu_eq0.doit()

        #update dict
        comb_dict[mu_eq1.lhs] = mu_eq1.rhs

        soln_item = f"""
        Now,

        $\\displaystyle
        {ltx(mu_p_eq5)}
        $

        Substitute: {dct_str}

        $\\displaystyle
        \\implies {ltx(mu_eq0)}
        $

        $\\displaystyle
        \\implies {ltx(mu_eq1)}
        $

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


        dct, dct_str = self._get_dict_and_str(comb_dict, keys = [mu, g, x0])

        intg_term_in_soln_1 = intg_term_in_soln.subs(dct)
        intg_term_in_soln_2 = intg_term_in_soln_1.doit()
        intg_term_in_soln_3 = factor(intg_term_in_soln_2)

        #update dict
        comb_dict[intg_term_in_soln] = intg_term_in_soln_3


        soln_item = f"""
        Next,

        $\\displaystyle
        {ltx(intg_term_in_soln)}
        $

        Substituting {dct_str}:

        $\\displaystyle
        = {ltx(intg_term_in_soln_1)}
        $

        $\\displaystyle
        = {ltx(intg_term_in_soln_2)}
        $

        $\\displaystyle
        = {ltx(intg_term_in_soln_3)}
        $

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

        dct, dct_str = self._get_dict_and_str(comb_dict,
                                              keys = [mu, x0, y0, intg_term_in_soln])


        y_soln_num_subs = y_soln_num.subs(dct)
        y_soln_den_subs = y_soln_den.subs(dct)

        #y_soln_eq_2 = y_soln_eq.subs(dct, evaluate = False)
        term1_symb = Symbol(latex_fraction(1, latex(y_soln_den_subs)))
        term2_symb = Symbol(f"\\left( {latex(y_soln_num_subs)} \\right)")
        term = y_soln_num_subs/ y_soln_den_subs

        y_soln_eq_2 = Eq(y, term1_symb * term2_symb)
        y_soln_eq_3 = simplify(Eq(y, term))


        soln_item = f"""
        Coming back to (ii),

        $\\displaystyle
        {ltx(y_soln_eq)}
        $

        Substitutting {dct_str}:

        $\\displaystyle
        \\implies {ltx(y_soln_eq_2)}
        $

        $\\displaystyle
        \\implies {ltx(y_soln_eq_3)}
        $


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

        y_soln_final_0 = Eq(y_soln_eq_3.lhs, factor(y_soln_eq_3.rhs))

        soln_item = f"""
        $\\displaystyle \\implies {ltx(y_soln_final_0)} $
        """
        soln_items.append(soln_item.strip())


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


        problem_s = f"""
        Solve:

        $\\displaystyle
        {ltx(eq)}
        $ with boundary values, {boundary_dict_str}.

        """

        answer_s = f"""
        $\\displaystyle {ltx(y_soln_final_0)} $
        """

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

        result.update(data)

        return result
        pass

    def _get_linear_first_order_set(self, x, y, keyword = None):
        dy_dx = Derivative(y, x)
        p = Function('p')(x)
        g = Function('g')(x)
        std_eq = Eq(dy_dx + p * y, g)

        x0 = Symbol(f"{latex(x)}_0")
        y0 = Symbol(f"{self._modified_latex(y)}_0")

        dicts = []

        expr_dict = {
            p: sympify(2),
            g: exp(-x)
            }

        boundary_dict = {
            x0: 0,
            y0: Rational(3, 4)
            }

        dicts.append([expr_dict, boundary_dict])

        expr_dict = {
            p: -2 * x,
            g: sympify(x)
            }

        boundary_dict = {
            x0: 0,
            y0: 0
            }

        dicts.append([expr_dict, boundary_dict])

        r = Symbol('r')
        k = Symbol('k')

        expr_dict = {
            p: - r,
            g: sympify(k)
            }

        boundary_dict = {
            x0: 0,
            y0: Symbol(f"y_0")
            }

        dicts.append([expr_dict, boundary_dict])



        expr_dict, boundary_dict = random.choice(dicts)
        eq = std_eq.subs(expr_dict)

        result = {
            'keyword': keyword,
            'x': x,
            'y': y,
            'x0': x0,
            'y0': y0,
            'dy_dx': dy_dx,
            'p': p,
            'g': g,
            'expr_dict': expr_dict,
            'boundary_dict': boundary_dict,
            'std_eq': std_eq,
            'eq': eq,
            }
        return result
        pass

    def get_linear_first_order_data(self, keyword = None):
        ltx = self._modified_latex

        x = self.x
        y = self.y
        z = self.z

        dx, dy = symbols(fr'd{x}, d{y}')
        # expr = x * ueval(dy) + y * ueval(dx)
        dz = symbols(f'd{z}')
        # zexpr = x * y
        data = self._get_linear_first_order_set(x, y, keyword = keyword)
        return data
        pass

    def _get_dict_and_str(self, dct, keys = None):
        if keys:
            dct = {k:v for k,v in dct.items() if k in keys}

        dct_str = [f"$\\displaystyle {latex(k)} = {latex(v)} $"
                             for k,v in dct.items()]
        dct_str = list_to_str(dct_str)
        return dct, dct_str
        pass


    def _get_reverse_soln_stmts(self, data):
        ltx = self._modified_latex

        zexpr = data['zexpr']
        dzexpr_dx = data['dzexpr_dx']
        x = data['x']
        y = data['y']

        var_symbs = [x, y]

        reverse_soln_stmts = []
        #reverse_soln_stmts.append("""<p style="color:red">Start of reverse_soln</p>""")

        d_solver = DerivativeSolver(zexpr, x, var_symbs,
                                             verbose = self.verbose)


        reverse_soln = d_solver.get_reverse_soln()

        # print r_dict
        r_dict = reverse_soln[-1]['r_dict']
        r_dict_stmts = self._print_r_dict(r_dict)
        if r_dict_stmts:
            #reverse_soln_stmts.extend(r_dict_stmts)
            pass


        #for sub_soln in reversed(reverse_soln[-2:]):
        for sub_soln in reverse_soln[-1:]:
            soln_items = self._convert_sub_soln_to_stmts(sub_soln, data)
            if not soln_items:
                continue

            if (not reverse_soln_stmts or reverse_soln_stmts[-1] == soln_items[0]):
                reverse_soln_stmts.extend(soln_items[1:])
            else:
                reverse_soln_stmts.extend(soln_items)
            pass

        #reverse_soln_stmts.append("""<p style="color:red">End of reverse_soln</p>""")

        return reverse_soln_stmts
        pass

    def _convert_sub_soln_to_stmts(self, sub_soln, data):
        ltx = self._modified_latex

        x = data['x']
        y = data['y']
        dx = data['dx']
        d_dx = latex_fraction("d", f"d{latex(x)}")

        soln_items = []
        #soln_items.append("""<p style="color:blue">Start of sub_soln</p>""")

        # print sub_soln dict
        sub_soln_items = self._print_sub_soln_items(sub_soln)
        if sub_soln_items:
            #soln_items.extend(sub_soln_items)
            pass

        expr = sub_soln['expr']
        org_ueval_args = sub_soln['org_ueval_args']
        mod_args = sub_soln['mod_args']
        r_args = sub_soln['r_args']
        r_dict = sub_soln['r_dict']



        mod_expr = type(expr)(*mod_args)
        stmt = f"""
        $\\displaystyle
            = \\left( {ltx(Derivative(mod_expr).doit(), mul_symbol='dot')} \\right)
            {latex(dx)}
            $
        """
        if (not soln_items or soln_items[-1] != stmt):
            soln_items.append(stmt.strip())


        r_expr = type(expr)(*r_args)
        r_expr_der = Derivative(r_expr).doit()
        r_expr_der_latex = ltx(r_expr_der, mul_symbol='dot')

        from itertools import cycle
        colors = cycle(['red', 'green', 'blue'])

        for k,v in r_dict.items():
            for v_pow in [1, 2, 3]:
                v_latex = f""" \\left( {ltx(v ** v_pow, mul_symbol='dot')} \\right) """
                v_latex = latex_color(v_latex, next(colors))
                r_expr_der_latex = r_expr_der_latex.replace(ltx(k ** v_pow, mul_symbol='dot'),
                                                            v_latex)
            pass

        stmt = f"""
        $\\displaystyle
            = \\left( {r_expr_der_latex} \\right)
            {latex(dx)}
            $
        """
        if (not soln_items or soln_items[-1] != stmt):
            soln_items.append(stmt.strip())



        org_ueval_expr = type(expr)(*org_ueval_args)
        stmt = f"""
        $\\displaystyle
            = {d_dx} \\left( {ltx(org_ueval_expr, mul_symbol='dot')} \\right)
            {latex(dx)}
            $
        """
        if (not soln_items or soln_items[-1] != stmt):
            soln_items.append(stmt.strip())


        #soln_items.append("""<p style="color:blue">End of sub_soln</p>""")

        return soln_items
        pass

    def _print_r_dict(self, r_dict):
        ltx = self._modified_latex

        r_dict_stmts = []
        stmt = f"""
        r_dict:
        """
        r_dict_stmts.append(stmt.strip())
        for k,v in r_dict.items():
            stmt = f"""
            $\\displaystyle
                {latex(k, mul_symbol='dot')}
                = {ltx(v, mul_symbol='dot')}
                $

            """
            r_dict_stmts.append(stmt.strip())
            pass
        return r_dict_stmts


    def _print_sub_soln_items(self, sub_soln):
        soln_items = []
        for k, v in sub_soln.items():
            try:
                stmt = f"""
                {k} = {v}
                """
                soln_items.append(stmt.strip())

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

            pass
        return soln_items
        pass

    def get_separable_functions_data(self, keyword = None):
        ltx = self._modified_latex

        a1, a2, a3, x, y = self.get_default_symbs('a1 a2 a3 x y'.split(" "))

        diff_eqs = []

        diff_eq = Eq(a1 * Derivative(y, x) + a2 * x + a3, 0)
        diff_eqs.append(diff_eq)

        diff_eq = Eq(a1 * Derivative(y, x) + a2 * y + a3, 0)
        diff_eqs.append(diff_eq)

        diff_eq = Eq((a1 * y + a2) * Derivative(y, x) , a3 * cos(x))
        diff_eqs.append(diff_eq)

        diff_eq = Eq(Derivative(y, x),
                     a1 * (a2 - y) * (a3 - y))
        diff_eqs.append(diff_eq)


        data = {
            'x': x,
            'y': y,
            'diff_eqs': diff_eqs,
        }

        return data
        pass

    def get_first_order_expr_data(self, keyword = None):
        ltx = self._modified_latex

        x = self.x
        y = self.y
        z = self.z

        dx, dy = symbols(fr'd{x}, d{y}')
        # expr = x * ueval(dy) + y * ueval(dx)
        dz = symbols(f'd{z}')
        # zexpr = x * y
        expr, zexpr = self._get_random_first_order_expr_set(x, y, dx, dy,
                                                            keyword = keyword)
        dz_dx = Derivative(z, x)
        dzexpr_dx_ueval = Derivative(zexpr, x)
        dzexpr_dx_ueval_ltx = latex_fraction(f"{{d}} ",
                                               f"{ltx(dx)}") + f"\\left( {ltx(zexpr, mul_symbol='dot')} \\right)"

        dzexpr_dx = Derivative(zexpr, x).doit()
        dzexpr = expr

        data = {
            'x': x,
            'y': y,
            'z': z,
            'z': z,
            'dx': dx,
            'dy': dy,
            'dz': dz,
            'expr': expr,
            'zexpr': zexpr,
            'dz_dx': dz_dx,
            'dzexpr_dx_ueval': dzexpr_dx_ueval,
            'dzexpr_dx_ueval_ltx': dzexpr_dx_ueval_ltx,
            'dzexpr_dx': dzexpr_dx,
            'dzexpr': dzexpr,
            'dx': dx
        }

        return data
        pass

    def _get_random_first_order_expr_set(self, x, y, dx, dy, keyword = None):
        expr_sets = self._get_first_order_expr_sets(x, y, dx, dy, keyword = keyword)
        return random.choice(expr_sets)
        pass

    def _get_first_order_expr_sets(self, x, y, dx, dy, keyword = None):
        m, n = symbols('m, n', constant = True )
        expr_sets = []

        expr = x * ueval(dy) + y * ueval(dx)
        zexpr = x * y
        expr_sets.append([expr, zexpr])

        expr = ueval(dy) + ueval(dx)
        zexpr = x + y
        expr_sets.append([expr, zexpr])

        expr = (x * ueval(dy) - y * ueval(dx))/(x**2)
        zexpr = y/x
        expr_sets.append([expr, zexpr])

        expr = (2 * x * y * ueval(dy) - y**2 * ueval(dx))/(x**2)
        zexpr = (y**2)/x
        expr_sets.append([expr, zexpr])

        expr = (2 * x**2 * y * ueval(dy) - 2 * x * y**2 * ueval(dx))/(x**4)
        zexpr = (y**2)/(x**2)
        expr_sets.append([expr, zexpr])

        expr = (x * ueval(dy) + y * ueval(dx))/(x*y)
        zexpr = log(x*y)
        expr_sets.append([expr, zexpr])

        expr = (x * ueval(dy) - y * ueval(dx))/(x*y)
        zexpr = log(y/x)
        expr_sets.append([expr, zexpr])

        expr = (ueval(dy) + ueval(dx))/(x + y)
        zexpr = log(x + y)
        expr_sets.append([expr, zexpr])

        expr = (x * ueval(dy) + y * ueval(dx))/(x**2 + y**2)
        zexpr = log(sqrt(x**2 + y**2))
        expr_sets.append([expr, zexpr])

        expr = (x * ueval(dy) - y * ueval(dx))/(x**2 + y**2)
        zexpr = atan(y/x)
        expr_sets.append([expr, zexpr])

        expr = (x * ueval(dy) + y * ueval(dx))/(x**2 * y**2)
        zexpr = -1/(x*y)
        expr_sets.append([expr, zexpr])

        expr = (x * exp(y) * ueval(dy) - exp(y) * ueval(dx))/(x**2)
        zexpr = exp(y)/x
        expr_sets.append([expr, zexpr])

        expr = (x * ueval(dy) + y * ueval(dx))/sqrt(x**2 + y**2)
        zexpr = sqrt(x**2 * y**2)
        expr_sets.append([expr, zexpr])

        expr = x**(m - 1) * y**(n - 1) * (m * y * ueval(dx) - n * x * ueval(dy))
        zexpr = x**m * y**n
        expr_sets.append([expr, zexpr])

        expr = (x * ueval(dy) - y * ueval(dx))/(x**2 - y**2)
        zexpr = Rational(1, 2) * log((x + y)/(x - y))
        expr_sets.append([expr, zexpr])

        expr = (ueval(dx)/(x**2)) - (ueval(dy)/(y**2))
        zexpr = (1/y) - (1/x)
        expr_sets.append([expr, zexpr])

        if keyword:
            expr_sets = [[expr, zexpr] for expr, zexpr in expr_sets
                         if (keyword in f"{expr}" or keyword in f"{zexpr}") ]

        return expr_sets
        pass

    def _modified_latex(self, expr, funct_symbs = None, mul_symbol = None):
        expr = self._convert_function_to_symbol(expr, funct_symbs = funct_symbs)
        expr_latex = latex(expr, mul_symbol = mul_symbol)

        expr_latex = expr_latex.replace("\\partial", "d")

        return expr_latex
        pass

    def _convert_function_to_symbol(self, expr, funct_symbs = None):
        if funct_symbs is None:
            funct_symbs = [self.y]

        temp_dict = {}
        for funct_symb in funct_symbs:
            temp_dict[funct_symb] = Symbol(funct_symb.name)
            pass

        if not temp_dict:
            return expr

        expr_subs = expr.subs(temp_dict)

        return expr_subs
        pass


    def get_diff_eq_type_examples_as_html(self):
        """
        Converts tuples like:
          (key, ex1, ex2, ex3)
        into an HTML string:

        <div class="diff-eq-list">
          <div class="diff-eq-item">
            <div class="name">Key:</div>
            <div class="examples">
              <div class="example">example 1</div>
              ...
            </div>
          </div>
        </div>

        Note: examples are LaTeX strings; render them on the page using MathJax/KaTeX if needed.
        """
        data = self.get_diff_eq_type_examples()

        parts = ['<div>']
        for key, row in data.items():
            name = row.get("name")
            examples = row.get("examples")

            parts.append('<div>')
            parts.append(f'<div><b>{name}:</b></div>')
            parts.append('<div>')

            for ex in examples:
                parts.append(f'<div style="margin-bottom: .5rem;">${ex}$</div>')

            parts.append('</div>')  # examples
            parts.append('<br/></div>')  # item

        parts.append('</div>')  # list
        html_str = "".join(parts)
        return html_str



    def get_diff_eq_type_examples(self, eq_type_key = None):
        '''
        @eq_type_key: if it is None, all types are returned. otherwise only that type

        '''
        data = self._get_data_diff_eq_type_examples()

        if eq_type_key is None:
            return data

        examples_dict = data.get(eq_type_key, {})
        return examples_dict
        pass


    def _get_diff_eq_type_names(self):
        '''
        returns a list
        '''

        data = self._get_data_diff_eq_type_examples()
        names = [x[0] for x in data.values()]
        return names
        pass

    def _get_diff_eq_type_keys(self):
        '''
        returns a list
        '''

        data = self._get_data_diff_eq_type_examples()
        return list(data.keys())
        pass

    def _get_data_diff_eq_type_examples(self):
        '''
        returns a dict
        '''

        data = {
            "ordinary_differential_equations": {
                "name": "Ordinary Differential Equations (ODEs)",
                "examples": [
                    r"\frac{dy}{dx} = x^2",
                    r"\frac{d^2y}{dx^2} + y = 0",
                    r"\frac{dy}{dx} + y = e^{x}",
                ],
            },
            "partial_differential_equations": {
                "name": "Partial Differential Equations (PDEs)",
                "examples": [
                    r"u_t = k\,u_{xx}",
                    r"u_{tt} = c^2\,u_{xx}",
                    r"u_{xx} + u_{yy} = 0",
                ],
            },
            "linear_differential_equations": {
                "name": "Linear Differential Equations",
                "examples": [
                    r"\frac{dy}{dx} + 3y = \sin x",
                    r"\frac{d^2y}{dx^2} - 2\frac{dy}{dx} + y = 0",
                    r"x\frac{dy}{dx} + y = x^2",
                ],
            },
            "nonlinear_differential_equations": {
                "name": "Nonlinear Differential Equations",
                "examples": [
                    r"\frac{dy}{dx} = y^2 - x",
                    r"\frac{d^2y}{dx^2} + \left(\frac{dy}{dx}\right)^2 = 0",
                    r"\frac{dy}{dx} = \sin(y)",
                ],
            },
            "homogeneous_linear_differential_equations": {
                "name": "Homogeneous (Linear: RHS = 0) Differential Equations",
                "examples": [
                    r"\frac{dy}{dx} - 4y = 0",
                    r"\frac{d^2y}{dx^2} + 5\frac{dy}{dx} + 6y = 0",
                    r"x^2\frac{d^2y}{dx^2} - x\frac{dy}{dx} + y = 0",
                ],
            },
            "nonhomogeneous_forced_linear_differential_equations": {
                "name": "Nonhomogeneous / Forced (Linear: RHS \neq 0) Differential Equations",
                "examples": [
                    r"\frac{dy}{dx} + y = e^{x}",
                    r"\frac{d^2y}{dx^2} + y = \cos x",
                    r"\frac{d^2y}{dx^2} - y = x",
                ],
            },
            "autonomous_differential_equations": {
                "name": "Autonomous Differential Equations",
                "examples": [
                    r"\frac{dy}{dt} = y(1-y)",
                    r"\frac{d^2y}{dt^2} = -y^3",
                    r"\frac{dx}{dt} = x - x^3",
                ],
            },
            "nonautonomous_differential_equations": {
                "name": "Nonautonomous Differential Equations",
                "examples": [
                    r"\frac{dy}{dt} = t\,y",
                    r"\frac{d^2y}{dt^2} + t\,y = 0",
                    r"\frac{dx}{dt} = \sin t - x",
                ],
            },

            "separable_first_order_ode": {
                "name": "Separable First-Order ODE",
                "examples": [
                    r"\frac{dy}{dx} = x\,y",
                    r"\frac{dy}{dx} = (1+x^2)(1+y^2)",
                    r"\frac{dy}{dx} = \frac{y+1}{x+1}",
                ],
            },
            "first_order_linear_ode": {
                "name": "First-Order Linear ODE",
                "examples": [
                    r"\frac{dy}{dx} + 2y = e^{x}",
                    r"\frac{dy}{dx} - \frac{1}{x}y = x^2",
                    r"\frac{dy}{dx} + y = \sin x",
                ],
            },
            "exact_first_order_ode": {
                "name": "Exact Differential Equation (First-Order)",
                "examples": [
                    r"(2xy+1)\,dx + (x^2+2y)\,dy = 0",
                    r"(y+2x)\,dx + (x+2y)\,dy = 0",
                    r"(e^{x}+y)\,dx + (x+1)\,dy = 0",
                ],
            },
            "bernoulli_equation": {
                "name": "Bernoulli Equation",
                "examples": [
                    r"\frac{dy}{dx} + y = y^2",
                    r"\frac{dy}{dx} + \frac{2}{x}y = x\,y^3",
                    r"\frac{dy}{dx} - y = e^{x}y^4",
                ],
            },
            "homogeneous_first_order_ode_yprime_f_y_over_x": {
                "name": "Homogeneous First-Order ODE (y' = F(y/x))",
                "examples": [
                    r"\frac{dy}{dx} = \frac{y}{x} + 1",
                    r"\frac{dy}{dx} = \left(\frac{y}{x}\right)^2",
                    r"\frac{dy}{dx} = \frac{y}{x} + \sin\!\left(\frac{y}{x}\right)",
                ],
            },

            "second_order_linear_ode_constant_coefficients": {
                "name": "Second-Order Linear ODE (Constant Coefficients)",
                "examples": [
                    r"y'' - 3y' + 2y = 0",
                    r"y'' + 4y = \sin t",
                    r"y'' + 2y' + y = e^{t}",
                ],
            },
            "conservative_newtonian_mechanics_form": {
                "name": "Conservative / Newtonian Mechanics Form (y'' = -V'(y))",
                "examples": [
                    r"y'' = -y",
                    r"y'' = -y^3",
                    r"y'' = -\sin y",
                ],
            },

            "autonomous_scalar_ode_phase_line": {
                "name": "Autonomous Scalar ODE / Phase-Line Equation (y' = f(y))",
                "examples": [
                    r"\frac{dy}{dt} = y(1-y)",
                    r"\frac{dy}{dt} = y^2 - 1",
                    r"\frac{dy}{dt} = \sin y",
                ],
            },
            "linear_system_of_odes": {
                "name": "Linear System of ODEs (x' = A x + b(t))",
                "examples": [
                    r"\frac{dx}{dt} = 2x,\ \frac{dy}{dt} = -y",
                    r"\frac{dx}{dt} = x+y,\ \frac{dy}{dt} = -x+y",
                    r"\frac{dx}{dt} = x+t,\ \frac{dy}{dt} = 2y - e^{t}",
                ],
            },

            "linear_pde": {
                "name": "Linear PDE",
                "examples": [
                    r"u_t = k\,u_{xx}",
                    r"u_x + u_y = 0",
                    r"u_{tt} - c^2 u_{xx} = 0",
                ],
            },
            "elliptic_pde": {
                "name": "Elliptic PDE",
                "examples": [
                    r"u_{xx} + u_{yy} = 0",
                    r"u_{xx} + u_{yy} + u = 0",
                    r"u_{xx} + 4u_{yy} = 0",
                ],
            },
            "parabolic_pde": {
                "name": "Parabolic PDE",
                "examples": [
                    r"u_t = k\,u_{xx}",
                    r"u_t = u_{xx} + u_{yy}",
                    r"u_t + a\,u_x = k\,u_{xx}",
                ],
            },
            "hyperbolic_pde": {
                "name": "Hyperbolic PDE",
                "examples": [
                    r"u_{tt} = c^2 u_{xx}",
                    r"u_{tt} - c^2(u_{xx} + u_{yy}) = 0",
                    r"u_{xy} = 0",
                ],
            },
            "first_order_nonlinear_pde": {
                "name": "First-Order Nonlinear PDE (Method of Characteristics)",
                "examples": [
                    r"u_t + u\,u_x = 0",
                    r"u_t + u^2 u_x = 0",
                    r"u_x + u\,u_y = 0",
                ],
            },
        }
        return data






    pass
#class DifferentialEquationHelper:
