'''
Created on 12-Jul-2021

@author: bthakur
'''
import random
import inspect
import traceback
import re
from operator import mul, add
from functools import reduce

import numpy as np
import sympy
from sympy import (symbols, Symbol, limit, latex, Function,
                   Limit, oo, Derivative, simplify,
                   sin, cos, tan, exp, log, sqrt, integer_nthroot,
                   Mul, Add, Pow
                   )

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.km.util import latex_fraction
from ._lhopitals_helper import LHopitalsHelper

from xv.util import UtilClass

class LimitHelper(UtilClass):
    '''
    Calculate Limit in calculus
    Example:
        from xv.math.calculus.helper import LimitHelper
        from sympy import *
        
        limitHelper = LimitHelper(verbose = True)
        
        x = Symbol('x')
        f = ((1 + 1 / x) ** x) **x
        x0 = oo
        
        result = limitHelper.get_solution(f, x, x0)
        result_str = limitHelper.solution_to_str(result)
        
    doc_style: xv_doc
    '''    
    
    _LABEL = "L"
    _MAIN_OPACITY = 1
    _SUB_OPACITY = 0.5

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


    def define_limit_at_a_point(self, **kwargs):
        x = kwargs.get('f', Symbol('x'))
        L = kwargs.get('L', Symbol('L'))
        f = kwargs.get('f', Function('f')(x))
        x0 = kwargs.get('x0', Symbol(f'{latex(x)}_0'))
        lower = kwargs.get('lower', Symbol('a'))
        upper = kwargs.get('upper', Symbol('b'))
        
        
        epsilon = Symbol('\\varepsilon')
        delta = Symbol('\\delta')
        
        ltx_x = f"$\\displaystyle {latex(x)}$"
        ltx_f = f"$\\displaystyle {latex(f)}$"
        ltx_L = f"$\\displaystyle {latex(L)}$"
        ltx_x0 = f"$\\displaystyle {latex(x0)}$"
        ltx_interval = f"$\\displaystyle \\left( {latex(lower)}, {latex(upper)} \\right)$"
        
        def_items = []

        def_item = f"""
        Let {ltx_x} be a function defined on the interval {ltx_interval}, 
        except possibly at {ltx_x}, where {ltx_x} is in the interval, i.e., 
        <br/>
        $\\displaystyle {latex(lower)} < {latex(x0)} < {latex(upper)} $
        """
        def_item = def_item.replace("\n", " ")
        def_items.append(def_item.strip())

        def_item = f"""
        """
        def_item = def_item.replace("\n", " ")
        def_items.append(def_item.strip())

        def_item = f"""
        Then the limit is of {ltx_f} at {ltx_x0} is defined as
        <br/>
        $\\displaystyle 
        \\lim _{{{latex(x)} \\rightarrow {latex(x0)}  }} {latex(f)} 
        = {latex(L)}$ 
        <br />
        if for every number 
        $\\displaystyle {latex(epsilon)} > 0 $ 
        there exists a 
        $\\displaystyle {latex(delta)} > 0 $ 
        such that
        
        <br/>
        $\\displaystyle
        \\left| {latex(f)} - {latex(L)}  \\right| < {latex(epsilon)}
        $
        whenever
        $\\displaystyle
        0 < \\left| {latex(x)} -  {latex(x0)} \\right| < {latex(delta)}
        $ for all {ltx_x}.        
        """
        def_item = def_item.replace("\n", " ")
        def_items.append(def_item.strip())

        limit_def = "\n".join(def_items)
        

        limit_expl = f"""
        In simple terms, as you go closer to {ltx_x0}, 
        the value of {ltx_f} converges to {ltx_L}
        """
        
        problem_s = r"""
        Define limit.
        """
        
        soln_s = f"""
        {limit_def}
        {limit_expl}
        """
        
        answer_s = f"""
        {limit_def}
        """

        title = f'Limit of $x^2$ at x=1'
        svg_str = self.plot_limit(x**2, x, 1, -2, 2,
                                  title = title)
        
        result = {
            'problem_s': problem_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            'svg_str': svg_str,
            }
        return result
        pass

    def define_limit_at_a_continuity(self, **kwargs):
        
        x = kwargs.get('f', Symbol('x'))
        f = kwargs.get('f', Function('f')(x))
        x0 = kwargs.get('x0', Symbol(f'{latex(x)}_0'))
        lower = kwargs.get('lower', Symbol('a'))
        upper = kwargs.get('upper', Symbol('b'))

        
        delta = Symbol('\\delta')

        f_minus_delta = f.subs(x, Symbol(f"{latex(x0)} - {latex(delta)}"))
        f_plus_delta = f.subs(x, Symbol(f"{latex(x0)} + {latex(delta)}"))
        f_x0 = f.subs(x, x0)
        
        ltx_x = f"$\\displaystyle {latex(x)}$"
        ltx_f = f"$\\displaystyle {latex(f)}$"
        ltx_x0 = f"$\\displaystyle {latex(x0)}$"
        ltx_interval = f"$\\displaystyle \\left( {latex(lower)}, {latex(upper)} \\right)$"
        
        def_items = []

        def_item = f"""
        Let {ltx_x} be a function defined on the interval {ltx_interval}, 
        in neighbourhood of {ltx_x} = {ltx_x0}, where {ltx_x} is in the interval, i.e., 
        <br/>
        $\\displaystyle {latex(lower)} < {latex(x0)} < {latex(upper)} $
        """
        def_item = def_item.replace("\n", " ")
        def_items.append(def_item.strip())

        def_item = f"""
        """
        def_item = def_item.replace("\n", " ")
        def_items.append(def_item.strip())

        Lim_x0_minus = Limit(f_minus_delta, delta, 0)
        Lim_x0_plus = Limit(f_plus_delta, delta, 0)

        def_item = f"""
        Then {ltx_f} is continuous at {ltx_x0} if
        <br/>
        <br/>
        $\\displaystyle 
        
        {latex(Lim_x0_minus)}
        
        = {latex(f_x0)}
        
        = {latex(Lim_x0_plus)}
        
        $ 
        """
        def_item = def_item.replace("\n", " ")
        def_items.append(def_item.strip())

        continuity_def = "\n".join(def_items)
        

        problem_s = r"""
        Define continuity.
        """
        
        soln_s = f"""
        {continuity_def}
        """
        
        answer_s = f"""
        {continuity_def}
        """

        title = f'Continuity of $x^2$ at x=1'
        svg_str = self.plot_continuity(x**2, x, 1, -2, 2,
                                  title = title)
        
        result = {
            'problem_s': problem_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            'svg_str': svg_str,
            }
        return result
        pass


    def define_algebra_of_limits(self, **kwargs):
        x = kwargs.get('f', Symbol('x'))
        f = kwargs.get('f', Function('f')(x))
        g = kwargs.get('g', Function('g')(x))
        h = kwargs.get('h', Function('h')(x))
        x0 = kwargs.get('x0', Symbol(f'{latex(x)}_0'))

        k = kwargs.get('k', Symbol('k'))
        n = kwargs.get('k', Symbol('n', integer = True))
        L = kwargs.get('k', Symbol('L'))
        

        f_Lim = Limit(f, x, x0)
        g_Lim = Limit(g, x, x0)
        h_Lim = Limit(h, x, x0)
        f_plus_g_Lim = Limit(f + g, x, x0)
        f_minus_g_Lim = Limit(f + g, x, x0)
        fg_Lim = Limit(f * g, x, x0)
        f_power_n_Lim = Limit(f ** n, x, x0)
        f_nth_root_Lim = Limit(sympy.Pow(f, 1/n), x, x0)
        
        f_by_g_Lim = Limit(f / g, x, x0)

        kf_Lim = Limit(k * f, x, x0)

        ltx_x = f"$\\displaystyle {latex(x)}$"
        ltx_f = f"$\\displaystyle {latex(f)}$"
        ltx_g = f"$\\displaystyle {latex(g)}$"
        ltx_h = f"$\\displaystyle {latex(h)}$"
        ltx_x0 = f"$\\displaystyle {latex(x0)}$"
        ltx_k = f"$\\displaystyle {latex(k)}$"
        
        
        ltx_f_Lim = f"$\\displaystyle {latex(f_Lim)}$"
        ltx_g_Lim = f"$\\displaystyle {latex(g_Lim)}$"
        ltx_h_Lim = f"$\\displaystyle {latex(h_Lim)}$"
        
        f_by_g_Lim_frac = latex_fraction(latex(f_Lim), latex(f_Lim))

        def_items = []

        def_item = f"""
        Suppose that {ltx_f} and {ltx_g} are functions
        and {ltx_x0} and {ltx_k} are real. 
        
        <br/>
        <br/>
        
        If both {ltx_f_Lim} and 
        {ltx_g_Lim} exist, then
        """
        def_item = def_item.replace("\n", " ")
        def_items.append(def_item.strip())

        sn = 0

        sn = sn + 1
        def_item = f"""
        <h4>{sn}. Limit of sum of functions:</h4>
        $\\displaystyle
        {latex(f_plus_g_Lim)}
        = {latex(f_Lim)} + {latex(g_Lim)}
        $
        
        $\\displaystyle
        {latex(f_minus_g_Lim)}
        = {latex(f_Lim)} - {latex(g_Lim)}
        $
        
        """
        def_items.append(def_item.strip())

        sn = sn + 1
        def_item = f"""
        <h4>{sn}. Limit of scaler multiplication of a function:</h4>
        $\\displaystyle
        {latex(kf_Lim)}
        = {latex(k)} \\; {latex(f_Lim)}
        $
        
        """
        def_items.append(def_item.strip())


        f_Lim_power_n = self._convert_term_to_symb(f_Lim) ** n
        f_Lim_nth_root = sympy.Pow(f_Lim, 1/n)
        
        sn = sn + 1
        def_item = f"""
        <h4>{sn}. Limit of product of functions:</h4>
        $\\displaystyle
        {latex(fg_Lim)}
        = {latex(f_Lim)} \\; {latex(g_Lim)}
        $
        
        $\\displaystyle
        {latex(f_power_n_Lim)}
        = {latex(f_Lim_power_n)}
        \\qquad \\text{{ where {latex(n)} is a positive integer }}
        $
        
        $\\displaystyle
        {latex(f_nth_root_Lim)}
        = {latex(f_Lim_nth_root)}
        \\qquad \\text{{ where {latex(n)} is a positive integer }}
        $
        
        
        """
        def_items.append(def_item.strip())


        sn = sn + 1
        def_item = f"""
        <h4>{sn}. Limit of division of functions:</h4>
        $\\displaystyle
        {latex(f_by_g_Lim)}
        = {f_by_g_Lim_frac}
        \\qquad
        \\text {{ provided }} 
        {latex(g_Lim)} \\ne {{0}}
        $
        """
        def_items.append(def_item.strip())


        sn = sn + 1
        def_item = f"""
        <h4>{sn}. Pinching Theorem: relation between values of functions:</h4>
        If $\\displaystyle
        {latex(f_Lim)} 
        \\le {latex(g_Lim)}
        \\le {latex(h_Lim)}
        $, and  
        
        $\\displaystyle
        {latex(f_Lim)} 
        = {latex(h_Lim)} 
        = {latex(L)}
        $, then 
          
        $\\displaystyle
        {latex(g_Lim)} 
        = {latex(L)}
        $
        
        """
        #def_item = def_item.replace("\n", " ")
        def_items.append(def_item.strip())

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

        problem_s = r"""
        Define continuity.
        """
        
      
        answer_s = f"""
        {soln_s}
        """


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


    def plot_limit(self, f, x, x0, a, b,
                           figsize = (10,6),
                           title = None,
                           use_cache = True
                           ):
        
        use_cache = False

        self.cacheUtil = CacheUtil(use_cache = use_cache, verbose = self.verbose)
        
        cache_file_path, exists_cached_file = self.cacheUtil.get_cached_path(
                self.__class__, inspect.stack()[0][3],
                f, x, x0, a, b,
                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()

        fig = plt.figure(figsize = figsize)
        ax = plt.gca()
        
        xs = np.linspace(a, b, 100)
        ys = xs ** 2
        
        ax.plot(xs, ys, color = (1, 0, 0, .3))

        x1 =  x0 - 0.1
        ax.plot(x1, x1**2, marker = 'o', markersize = 6, color = "blue")
        ax.annotate(f"$f({x0}-\\delta)=({x0}-\\delta)^2$", 
                    xy = (x1, x1**2 - 0.2),
                    color = "blue" )


        ax.plot((x1, x1), (x1**2, x0**2), color = "blue")

        ax.axhline(y=x0**2, color = (0, 0, 0, .3))
        ax.annotate(f"L={x0**2}", xy = (x0 - .5, x0**2) )


        x1 =  x0 + 0.1
        ax.plot(x1, x1**2, marker = 'o', markersize = 6, color = "green")
        ax.annotate(f"$f({x0}+\\delta)=({x0}+\\delta)^2$", 
                    xy = (x1, x1**2 + 0.2),
                    color = "green" )


        ax.plot((x1, x1), (x1**2, x0**2), color = "green")
        ax.annotate(f"$\\varepsilon$", xy = (x1 + 0.05, (x0**2 + x1**2)/2), color = "green" )

        expl_text = f"""
        The difference between $f({x0}+\\delta)$ and $L$ is $\\varepsilon$.
        We can reduce $\\varepsilon$ as much as we want by reducing 
        the value of $\\delta$.
        """
        ax.text(-1.5, 2.5, expl_text)
        
        ax.set_title(title, color =  (1, 0, 0, 1))
            
        svgHelper = SvgHelper()
        svg_str = svgHelper.plt_to_svg()
    
        if not svg_str:
            if self.verbose:
                print(f"Error: svg_str could not be initialized")
            return None
            
        self.cacheUtil.save_file(svg_str, cache_file_path)
        
        plt.clf()
        plt.close()
        
        return svg_str
        pass

    def plot_continuity(self, f, x, x0, a, b,
                           figsize = (10,6),
                           title = None,
                           use_cache = True
                           ):
        
        use_cache = False

        self.cacheUtil = CacheUtil(use_cache = use_cache, verbose = self.verbose)
        
        cache_file_path, exists_cached_file = self.cacheUtil.get_cached_path(
                self.__class__, inspect.stack()[0][3],
                f, x, x0, a, b,
                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()

        fig = plt.figure(figsize = figsize)
        ax = plt.gca()
        
        xs = np.linspace(a, b, 100)
        ys = xs ** 2
        
        ax.plot(xs, ys, color = (1, 0, 0, .3))

        x1 =  x0 - 0.1
        ax.plot(x1, x1**2, marker = 'o', markersize = 6, color = "blue")
        ax.annotate(f"$f({x0}-\\delta)=({x0}-\\delta)^2$", 
                    xy = (x1, x1**2 - 0.2),
                    color = "blue" )

        ax.plot(x0, x0**2, marker = 'o', markersize = 6, color = "red")
        ax.annotate(f"$f({x0})=({x0})^2$", 
                    xy = (x0 + 0.2, x0**2),
                    color = "red" )

        x1 =  x0 + 0.1
        ax.plot(x1, x1**2, marker = 'o', markersize = 6, color = "green")
        ax.annotate(f"$f({x0}+\\delta)=({x0}+\\delta)^2$", 
                    xy = (x1, x1**2 + 0.2),
                    color = "green" )


        expl_text = f"""
        $f(x)$ is continuous at ${x}={x0}$ if all three points become
        the same if $\\delta$ becomes almost zero.
        """
        ax.text(-1.5, 2.5, expl_text)
        
        ax.set_title(title, color =  (1, 0, 0, 1))
            
        svgHelper = SvgHelper()
        svg_str = svgHelper.plt_to_svg()
    
        if not svg_str:
            if self.verbose:
                print(f"Error: svg_str could not be initialized")
            return None
            
        self.cacheUtil.save_file(svg_str, cache_file_path)
        
        plt.clf()
        plt.close()
        
        return svg_str
        pass

    def _init_solution_config(self):
        self._solution_idx = 0
        
    def _next_solution_idx(self):
        self._solution_idx = self._solution_idx + 1
        return self._solution_idx

    def get_solution(self, f, x, x0):
        self._init_solution_config()
        
        solution = self._get_solution_step(f, x, x0)
        return solution
        pass

    def _get_solution_step(self, f, x, x0, print_answer = False):
        solution_idx = self._next_solution_idx()
        label = Symbol(f"{LimitHelper._LABEL}_{{{solution_idx}}}")
        
        f_Lim = Limit(f, x, x0)
        
        soln_statement = []
        if solution_idx == 1:
            statement = f"""
            Solve $\\displaystyle 
            {latex(label)} 
            = {latex(f_Lim)}
            $
            """.strip()
            
            soln_statement.append(statement)

        answer  = self._get_limit(f, x, x0)
        if answer is None:
            child_solutions = None
            self._log(f"{f} at {x} = {x0}")
            answer_statement = f"""
            Failed to get limit of 
            $\\displaystyle 
            {latex(label)} 
            = {latex(f_Lim)}
            $
            """
            answer = "Failed to get limit"
        else:
            child_solutions = self._get_solution_children(f, x, x0, label)
            
            answer_statement = None
            
            if solution_idx == 1 or child_solutions:
                print_answer = True
            
            if print_answer:
                if len(soln_statement) > 0 or child_solutions:
                    sTherefore = "Therefore, "
                else:
                    sTherefore = ""
    
                answer_statement = f"""
                {sTherefore}$\\displaystyle 
                {latex(label)} 
                = {latex(f_Lim)}
                = {latex(answer)}
                $
                """
            pass
        
        soln_statement = "\n\n".join(soln_statement)
        
        solution_step = {
            'solution_idx': solution_idx,
            'label': label,
            'method_name': inspect.stack()[0][3],
            'f': f,
            'x': x,
            'x0': x0,
            'Limit': f_Lim,
            'statement': soln_statement,
            'answer_statement': answer_statement,
            'answer': answer,
            'children': child_solutions
            }
        
        return solution_step
        pass
    
    def _get_solution_children(self, f, x, x0, parent_label):
        child_solutions = {}

        f_explog = self._explog_format(f, x) 
        if not f_explog is None:
            child_solution = self._explog_solution(
                                    f, f_explog, x, x0, parent_label)
            child_solutions['explog'] = child_solution
            return child_solutions
        
        mul_args = self._mul_args(f, x) 
        if not mul_args is None:
            lHopitalsHelper = LHopitalsHelper(verbose = self.verbose)

            is_lhopital_expr = lHopitalsHelper._is_lhopital_expr(f, x, x0)
            
            if not is_lhopital_expr:
                child_solution = self._mul_solution(
                                        f, mul_args, x, x0)
                if child_solution:
                    child_solutions['Mul'] = child_solution
                    return child_solutions
                pass
            else:
                child_solution = lHopitalsHelper.get_solution(
                                    f, x, x0, 
                                    solution_idx = self._solution_idx, 
                                    parent_label = parent_label)
                if child_solution:
                    child_solutions['LHopitals'] = child_solution
                    return child_solutions
                pass
            pass
        
        add_args = self._add_args(f, x) 
        if not add_args is None:
            child_solution = self._add_solution(
                                    f, add_args, x, x0)
            if child_solution:
                child_solutions['Add'] = child_solution
                return child_solutions
                
            pass
        
        return None
        pass

    def _add_solution(self, f, add_args, x, x0):
        solution_idx = self._solution_idx #self._next_solution_idx()
        label = Symbol(f"{LimitHelper._LABEL}_{{{solution_idx}}}")

        f_Lim = Limit(f, x, x0)
        
        lmt_stmt = f"\\lim _{{x \\rightarrow {latex(x0)}}}"
        
        add_args_Lims = [Limit(term, x, x0)
                        for term in add_args
                        ]
        
        add_args_str = [f"{latex(term)}"
                             for term in add_args]
        add_args_str = " + ".join(add_args_str)
        
        
        add_args_Lims_str = [f"{latex(term)}"
                             for term in add_args_Lims]
        add_args_Lims_str = " + ".join(add_args_Lims_str)
        
        sub_solutions = []
        answers = []
        labels = []
        for add_arg in add_args:
            sub_solution = self._get_solution_step(add_arg, x, x0,
                                                   print_answer = True)

            labels.append(sub_solution['label'])
            answers.append(sub_solution['answer'])
            sub_solutions.append(sub_solution)
            pass
        
        labels_str = [f"{latex(term)}" for term in labels]
        labels_str = " + ".join(labels_str)
        
        answers_str = [f"{latex(term)}" for term in answers]
        answers_str = " + ".join(answers_str)
        
        answer = reduce(add, answers)
            
        start_statement = f"""
        <b>Convert expression into sum of terms:</b>
        
        $\\displaystyle 
        {latex(label)} = {latex(f_Lim)}
        $

        $\\displaystyle 
        = {lmt_stmt}\\left( {add_args_str} \\right)
        $

        $\\displaystyle 
        = {add_args_Lims_str}
        $

        $\\displaystyle 
        = {labels_str}
        $
        """.strip()
        
        pre_statement = f"""
        <b>Compute each term separately:</b>
        """.strip()
        
        post_statement = f"""
        <b>Compute sum of result of terms:</b>
        
        $\\displaystyle 
        {latex(label)} = {latex(f_Lim)}
        $

        $\\displaystyle 
        = {add_args_Lims_str}
        $

        $\\displaystyle 
        = {labels_str}
        $

        $\\displaystyle 
        = {answers_str}
        $

        $\\displaystyle 
        = {latex(answer)}
        $
        """.strip()
        
        solution_step = {
            'solution_idx': solution_idx,
            'label': label,
            'method_name': inspect.stack()[0][3],
            'f': f,
            'x': x,
            'x0': x0,
            'Limit': f_Lim,
            'type': 'Mul',
            'start_statement': start_statement,
            'pre_statement': pre_statement,
            'post_statement': post_statement,
            'sub_solutions': sub_solutions,
            }
        return solution_step
        pass
    
    def _mul_solution(self, f, mul_args, x, x0):
        solution_idx = self._solution_idx #self._next_solution_idx()
        label = Symbol(f"{LimitHelper._LABEL}_{{{solution_idx}}}")

        f_Lim = Limit(f, x, x0)
        
        lmt_stmt = f"\\lim _{{x \\rightarrow {latex(x0)}}}"
        
        mul_args_Lims = [Limit(self._convert_term_to_symb(term), x, x0)
                        for term in mul_args
                        ]
        
        mul_args_str = [f"{self._convert_term_to_symb(term)}"
                             for term in mul_args]
        mul_args_str = " * ".join(mul_args_str)
        
        
        
        mul_args_Lims_str = [f"{latex(term)}"
                             for term in mul_args_Lims]
        mul_args_Lims_str = " * ".join(mul_args_Lims_str)

        sub_solutions = []
        labels = []
        answers = []
        for mul_arg in mul_args:
            sub_solution = self._get_solution_step(mul_arg, x, x0,
                                                   print_answer = True)

            labels.append(sub_solution['label'])
            answers.append(sub_solution['answer'])
            sub_solutions.append(sub_solution)
            pass
        
        labels_str = [f"{latex(term)}" for term in labels]
        labels_str = " * ".join(labels_str)
        
        answers_str = [f"{latex(term)}" for term in answers]
        answers_str = " * ".join(answers_str)
        
        if (0 in answers) and (oo in answers):
            answer = "\\text{{Invalid limit}}\\"
        else:
            answer = reduce(mul, answers)
        
        start_statement = f"""
        <b>Convert expression into factors:</b>
        
        $\\displaystyle 
        {latex(label)} = {latex(f_Lim)}
        $

        $\\displaystyle 
        = {lmt_stmt}\\left( {mul_args_str} \\right)
        $

        $\\displaystyle 
        = {mul_args_Lims_str}
        $

        $\\displaystyle 
        = {labels_str}
        $
        """.strip()
        
        pre_statement = f"""
        <b>Compute each factor separately:</b>
        """.strip()
        
        post_statement = f"""
        <b>Combine result of factors:</b>
        
        $\\displaystyle 
        {latex(label)} = {latex(f_Lim)}
        $

        $\\displaystyle 
        = {mul_args_Lims_str}
        $

        $\\displaystyle 
        = {labels_str}
        $

        $\\displaystyle 
        = {answers_str}
        $

        $\\displaystyle 
        = {latex(answer)}
        $
        """.strip()
        
        solution_step = {
            'solution_idx': solution_idx,
            'label': label,
            'method_name': inspect.stack()[0][3],
            'f': f,
            'x': x,
            'x0': x0,
            'Limit': f_Lim,
            'type': 'Mul',
            'start_statement': start_statement,
            'pre_statement': pre_statement,
            'post_statement': post_statement,
            'sub_solutions': sub_solutions,
            }
        return solution_step
        pass
    
    def _explog_solution(self, f, f_explog, x, x0, parent_label):
        solution_idx = self._solution_idx #self._next_solution_idx()
        label = Symbol(f"{LimitHelper._LABEL}_{{{solution_idx}}}")

        f_Lim = Limit(f, x, x0)
        
        f_exp = f_explog.args[0]
        f_exp_form = exp(f_exp)
        f_exp_Lim = Limit(f_exp, x, x0)
        
        f_exp_form_Lim1 = Limit(f_exp_form, x, x0)
        f_exp_form_Lim2 = exp(f_exp_Lim)
        
        sub_solution = self._get_solution_step(f_exp, x, x0,
                                               print_answer = True)
        label_exp = sub_solution['label']
        answer_exp = sub_solution['answer']
        answer = exp(answer_exp)

        start_statement = f"""
        <b>Convert expression to exponent and log format:</b>
        
        $\\displaystyle 
        {latex(parent_label)} = {latex(f_Lim)}
        = {latex(f_exp_form_Lim1)}
        = {latex(f_exp_form_Lim2)}
        = e^{{{latex(label_exp)}}}
         $
        """.strip()
        
        pre_statement = f"""
        <b>Compute exponent part:</b>
        """.strip()
        
        post_statement = f"""
        <b>Put limit of exponent part:</b>

        $\\displaystyle 
        {latex(parent_label)} 
        = {latex(f_Lim)}
        $

        $\\displaystyle 
        = {latex(f_exp_form_Lim1)}
        = {latex(f_exp_form_Lim2)}
        = e^{{{latex(label_exp)}}}
        = e^{{{latex(answer_exp)}}}
        = {latex(answer)}
        $
        """.strip()
        
        solution_step = {
            'solution_idx': solution_idx,
            'label': label,
            'method_name': inspect.stack()[0][3],
            'f': f,
            'x': x,
            'x0': x0,
            'type': 'explog',
            'start_statement': start_statement,
            'pre_statement': pre_statement,
            'post_statement': post_statement,
            'Limit': f_exp_Lim,
            'sub_solutions': [sub_solution]
            }
        return solution_step
        pass
    

    def _explog_format(self, f, x):
        if not self._is_pow_of_var(f, x):
            return None
        
        f1 = exp(f.args[1] * log(f.args[0]))
        return f1
        pass

    def solution_to_str(self, solution):
        solution_str = self._solution_to_str(solution)
        return solution_str

    def _solution_to_str(self, solution):
        #keys = list(solution.keys())
        statements = []

        solution_idx = solution.get('solution_idx', None)

        method_name = solution.get('method_name', None)
        if self.verbose:
            statements.append(f"solution_idx: {solution_idx}:: method_name: {method_name}")

        start_statement = solution.get('start_statement', None)
        if start_statement:
            statements.append(start_statement)
            
        pre_statement = solution.get('pre_statement', None)
        if pre_statement:
            statements.append(pre_statement)
            
        statement = solution.get('statement', None)
        if statement:
            statements.append(statement)
            
        sub_solutions = solution.get('sub_solutions', None)
        if sub_solutions:
            for sub_solution in sub_solutions:
                sub_solution_statement = self._solution_to_str(sub_solution)
                if sub_solution_statement:
                    #sub_solution_method_name = sub_solution['method_name']
                    #statements.append(f'#START: sub_solution_statement {sub_solution_method_name}')
                    statements.append(sub_solution_statement)
                    #statements.append(f'##END:  sub_solution_statement end {sub_solution_method_name}')
            
        children = solution.get('children', None)
        if children:
            for k, v in children.items():
                if not isinstance(v, dict):
                    self._log(f"v is not a dict: {type(v)}\n{v}")
                    continue
                child_statement = self._solution_to_str(v)
                if child_statement:
                    statements.append(child_statement)
                pass
            pass

        post_statement = solution.get('post_statement', None)
        if post_statement:
            statements.append(post_statement)
        
        answer_statement = solution.get('answer_statement', None)
        if answer_statement:
            statements.append(answer_statement)
        
        statements = "\n\n".join(statements)
        return statements
        pass
    

    def _mul_args(self, f, x):
        if not isinstance(f, Mul):
            return None
        terms = [t for t in f.args
                 if x in t.free_symbols]
        
        if len(terms) < 2:
            return None
        
        return f.args
        pass
    
    def _add_args(self, f, x):
        if not isinstance(f, Add):
            return None
        
        return f.args
        pass
    

    def _is_pow_of_var(self, f, x):
        if not isinstance(f, Pow):
            return False
        
        if x in f.args[1].free_symbols:
            return True
        
        return False
        pass
    
    def _is_lhopital_expr(self, f, x, x0):
        mul_args = self._mul_args(f, x) 
        answers = [limit(term, x, x0) 
                   for term in mul_args]
        
        if (0 in answers) and (oo in answers):
            return True
        
        return False
        pass
    
    def get_random_limit_expr(self):
        x = Symbol("x")

        exprs = self._get_random_limit_expr(x)

        lHopitalsHelper = LHopitalsHelper(verbose = self.verbose)
        expr_set1 = lHopitalsHelper.get_exprs_level1(x = x)
        expr_set2 = lHopitalsHelper.get_exprs_level2(x = x)

        expr_set1 = [(ueval(expr[0]) / expr[1], expr[2]) for expr in expr_set1]
        expr_set2 = [(ueval(expr[0]) / expr[1], expr[2]) for expr in expr_set2]

        #exprs.extend(expr_set1)
        #exprs.extend(expr_set2)
        
        f, x0 = random.choice(exprs) 
        
        return f, x, x0
        pass

    def _get_random_limit_expr(self, x):
        exprs = []

        t1 = sin(x)
        t2 = x * (1 + cos(x))
        expr = [ueval(t1) / t2 , 0]
        exprs.append(expr) #it is not working
        

        a = Symbol('a', positive = True)
        b = Symbol('b', positive = True)
        c = Symbol('c', positive = True)
        t1 = cos(a * x) - cos(b * x)
        t2 = cos(c * x) - 1
        expr = [ueval(t1) / t2 , 0]
        exprs.append(expr) #it is not working
        

        a = Symbol('a', positive = True)
        t1 = sqrt(a + 2 * x) - sqrt(3 * x)
        t2 = sqrt(3 * a + x) - 2 *sqrt(x)
        expr = [ueval(t1) / t2 , a]
        exprs.append(expr)
        
        t1 = tan(x) - sin(x)
        t2 = sin(x)**3
        expr = [ueval(t1) / t2 , 0]
        exprs.append(expr)
        
        t1 = 2 * sin(x)**2 + sin(x) - 1
        t2 = 2 * sin(x)**2 - 3 * sin(x) + 1
        expr = [ueval(t1) / t2 , sympy.pi / 6]
        exprs.append(expr)
        

        t1 = sin(ueval(2 + x))        
        t2 = sin(ueval(2 - x))        
        expr = [(t1 - t2) / x , 0]
        exprs.append(expr)
        
        m = random.randint(2, 6)
        n = random.randint(2, 6)
        
        expr = [ueval((x**n - m**n)) / (x - m) , m]
        exprs.append(expr)
        
        expr = [(sqrt(2 + x) - sqrt(2)) / x , 0]
        exprs.append(expr)
        
        return exprs
    
    def _get_limit(self, f, x, x0):
        try:
            f = simplify(f)
            limit_value = sympy.limit(f, x, x0)
            return limit_value
        except Exception as e:
            sError = f"""Error: {self.__class__.__name__}.{inspect.stack()[0][3]}: {e}
            f = {f}
            x = {x}
            x0 = {x0}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            pass
        return None
    
    def _convert_term_to_symb(self, term):
        ops = ['+', '-', '*', '/', 'log']
        ops = ['+', '-', '/', 'log']
        
        if isinstance(term, Add):
            return Symbol(f"\\left({latex(term)}\\right)")
        
        for op in ops:
            if op in f"{term}":
                return Symbol(f"\\left({latex(term)}\\right)")
            pass
        return Symbol(f"{latex(term)}")
        pass
    

    pass
#class LimitHelper:
