'''
Created on 20-Jan-2022

@author: bthakur
'''

import inspect
import traceback
import random
import sympy
from sympy import (symbols, Symbol, Pow, latex, Rational, 
                   nsimplify, 
                   UnevaluatedExpr as ueval)

from xv.km.util import latex_fraction
from xv.util import toIntOrFloat, list_to_str
from xv.km._sympy_extension import CustomLogarithm as Log 
from xv.util import strip_source_code_tabs, get_function_source_code
import matplotlib.pyplot as plt
from xv.km.ui import SvgHelper   
from xv.plotter import WavesPlotter2d
      


# _logarithm_helper
# LogarithmHelper

from xv.util import UtilClass

class LogarithmHelper(UtilClass):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.default_symbs = self._init_default_symbs()
        pass

    def problem_concept_of_log(self, b):
        if b == 10:
            return self._problem_concept_of_log_on_base_10()
        return self._problem_concept_of_log(b)
        pass

    def _problem_concept_of_log(self, b):
        prob_s = f"""
        Explain the concept of log on base ${latex(b)}$.
        """
        data = [1, 2, 3, 6, 9, 12]
        
        soln_s = []
        
        stmts1 = [f"<b>We write numbers like this:</b>"]
        stmts2 = [f"<b>If I ask you to tell me only powers of {b} in the number, your answer will be:</b>"]
        stmts3 = [f"<b>So, let us summarize:</b>"]
        stmts4 = [f"<b>Base of power is base of log too.</b>"]
        stmts5 = [f"<b>Log of numbers:</b>"]
        for p in data:
            num = b ** p
            num_ueval = ueval(b) ** ueval(p)
                
            stmt1 = f"""
            {num} = $\\displaystyle {latex(num_ueval)}$ 
            """
            stmts1.append(stmt1.strip())

            stmt2 = f"""
            Power of {b} in {num} = {p}
            Power of {b} in $\\displaystyle {latex(num_ueval)}$ = {p}
            """
            stmts2.append(stmt2.strip())

            stmt3 = f"""
            {num} = $\\displaystyle {latex(num_ueval)}$ 
            log({num}) = {p}
            log($\\displaystyle {latex(num_ueval)}$) = {p}
            """
            stmts3.append(stmt3.strip())

            stmt4 = f"""
            $\\displaystyle log_{{{b}}}({num}) = {p}$ \t\t read as log of {num} on base {b} 
            $\\displaystyle log_{{{b}}}({latex(num_ueval)}) = {p}$ \t\t read as log of $\\displaystyle {latex(num_ueval)}$ on base {b}
            """
            stmts4.append(stmt4.strip())

            stmt5 = f"""
            $\\displaystyle log_{{{b}}}({num}) = {p}$ 
            $\\displaystyle log_{{{b}}}({latex(num_ueval)}) = {p}$ 
            """
            stmts5.append(stmt5.strip())

            pass
        
        stmts1 = "\n".join(stmts1)
        soln_s.append(stmts1.strip())

        stmts2.append(f"<b>And, the shortcut of telling power only is called log.</b>")
        
        stmts2 = "\n\n".join(stmts2)
        soln_s.append(stmts2.strip())

        stmts3 = "\n\n".join(stmts3)
        soln_s.append(stmts3.strip())
        
        stmts4 = "\n\n".join(stmts4)
        soln_s.append(stmts4.strip())
        
        stmts5 = "\n\n".join(stmts5)
        soln_s.append(stmts5.strip())
        
        soln_s = "\n\n".join(soln_s)

        answer_s = stmts5
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass

    def _problem_concept_of_log_on_base_10(self):
        b = 10
        prob_s = f"""
        Explain the concept of log on base ${latex(b)}$.
        """
        data = [('ten', 1),
                ('Hundred', 2),
                ('Thousand', 3),
                ('Million', 6),
                ('Billion', 9),
                ('Trillion', 12),
                ]
        
        soln_s = []
        
        stmts1 = [f"<b>We write numbers like this:</b>"]
        stmts2 = [f"<b>If I ask you to tell me only zeroes in the number, your answer will be:</b>"]
        stmts3 = [f"<b>So, let us summarize:</b>"]
        stmts4 = [f"<b>Log of numbers:</b>"]
        for num, p in data:
            if p == 1:
                txt_zero = 'zero'
            else:
                txt_zero = 'zeroes'
                
            stmt1 = f"""
            {num} = {b ** p} (1 followed by {p} {txt_zero})
            """
            stmts1.append(stmt1.strip())

            stmt2 = f"""
            Number of zeroes in {num} = {p}
            Number of zeroes in {b ** p} = {p}
            """
            stmts2.append(stmt2.strip())

            stmt3 = f"""
            {num} = {b ** p} (1 followed by {p} {txt_zero}.)
            log({num}) = {p}
            log({b ** p}) = {p}
            """
            stmts3.append(stmt3.strip())

            stmt4 = f"""
            log({num}) = {p}
            log({b ** p}) = {p}
            """
            stmts4.append(stmt4.strip())

            pass
        
        stmts1 = "\n".join(stmts1)
        soln_s.append(stmts1.strip())

        stmts2.append(f"<b>And, the shortcut of telling a number only by zeroes is called log.</b>")
        
        stmts2 = "\n\n".join(stmts2)
        soln_s.append(stmts2.strip())

        stmts3 = "\n\n".join(stmts3)
        soln_s.append(stmts3.strip())
        
        stmts4 = "\n\n".join(stmts4)
        soln_s.append(stmts4.strip())
        
        soln_s = "\n\n".join(soln_s)
        
        answer_s = stmts4
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass


    def problem_simple_log_expr(self, b1, b1_expr, e1):
        arg1 = ueval(b1) ** ueval(e1)
        answer = e1

        num_latex1 = f"""
        {b1_expr}
        """

        log1 = Log(arg1, b1, f"\\left({num_latex1}\\right)", latex(b1))
        log2 = Log(arg1, b1)
        
        
        
        prob_s = f"""
        Find
        $\\displaystyle  {log1}$
        """

        soln_s = f"""
        $\\displaystyle  {log1}$

        $\\displaystyle = {log2}$

        When base of logarithm and base of exponent are same, the value of log is equal to exponent
        
        $\\displaystyle = {latex(answer)}$
        """

        answer_s = f"""
        $\\displaystyle {latex(answer)}$
        """
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass


    def problem_find_log_of_product_series(self, b1, e1_large):
        arg1 = ueval(b1) ** ueval(e1_large)
        answer = e1_large

        num_latex1 = f"""
        {b1} * {b1} * {b1}  * .... {e1_large}\\space times
        """

        log1 = Log(arg1, b1, f"\\left({num_latex1}\\right)", latex(b1))
        
        log2 = Log(arg1, b1)
        
        
        
        prob_s = f"""
        Find
        $\\displaystyle  {log1}$
        """

        soln_s = f"""
        $\\displaystyle  {log1}$

        $\\displaystyle = {log2}$

        When base of logarithm and base of exponent are same, the value of log is equal to exponent
        
        $\\displaystyle = {latex(answer)}$
        """

        answer_s = f"""
        $\\displaystyle {latex(answer)}$
        """
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass


    def problem_find_log_of_product_of_pairs(self, b1, b1_expr, b2_expr, e1, e2):
        e_num = e1 
        e_den = e2 
        answer = e_num + e_den

        arg1_1 = ueval(b1) ** ueval(e1)
        arg1_2 = ueval(b1) ** ueval(e2)
        arg1 = arg1_1 * arg1_2

        num_latex1 = f"""\\left({b1_expr}\\right) 
                        \\left({b2_expr}\\right)"""

        log1 = Log(arg1, b1, f"\\left({num_latex1}\\right)", latex(b1))
        
        log2 = Log(arg1, b1)

        
        arg5 = ueval(b1) ** (ueval(e_num) + ueval(e_den))

        log5 = Log(arg5, b1)
        
        arg6 = ueval(b1) ** (ueval(answer))

        log6 = Log(arg6, b1)
        
        
        
        prob_s = f"""
        Find
        $\\displaystyle  {log1}$
        """

        soln_s = f"""
        $\\displaystyle  {log1}$
        
        $\\displaystyle = {log2}$

        $\\displaystyle = {log5}$

        $\\displaystyle = {log6}$

        $\\displaystyle = {latex(answer)}$
        """

        answer_s = f"""
        $\\displaystyle {latex(answer)}$
        """
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass


    def problem_log_of_division(self, b1, b1_expr, b2_expr, e1, e2):
        answer = e1 - e2

        num_latex1 = latex_fraction(f"{b1_expr}", 
                                   f"{b2_expr}")

        arg1_num = ueval(b1) ** ueval(e1)
        arg1_den = ueval(b1) ** ueval(e2) 
        arg1 = arg1_num / arg1_den
        
        log1 = Log(arg1, b1, f"\\left({num_latex1}\\right)", latex(b1))

        num_latex2 = latex_fraction(f"{latex(arg1_num)}", 
                                   f"{latex(arg1_den)}")

        log2 = Log(arg1, b1, f"\\left({num_latex2}\\right)", latex(b1))


        arg5 = ueval(b1) ** (ueval(e1) - ueval(e2))

        log5 = Log(arg5, b1)
        
        arg6 = ueval(b1) ** (ueval(answer))

        log6 = Log(arg6, b1)
        
        prob_s = f"""
        Find
        $\\displaystyle  {log1}$
        """

        soln_s = f"""
        $\\displaystyle {log1}$
        
        $\\displaystyle = {log2}$
        
        $\\displaystyle = {log5}$

        $\\displaystyle = {log6}$

        $\\displaystyle = {latex(answer)}$
        """

        answer_s = f"""
        $\\displaystyle {latex(answer)}$
        """
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass



    def problem_find_log_of_exp_to_exp(self, b1, b1_expr, e1, e3):
        e_num = e1 * e3
        answer = e_num


        arg1 = ueval(ueval(b1) ** ueval(e1)) ** ueval(e3)

        num_latex1 = f"""\\left({b1_expr}\\right)^{e3}"""

        log1 = Log(arg1, b1, f"\\left({num_latex1}\\right)", latex(b1))
        
        log2 = Log(arg1, b1)

        arg3 = ueval(b1) ** (ueval(e1) * ueval(e3))
        
        log3 = Log(arg3, b1)
        
        arg4 = ueval(b1) ** (ueval(answer))
        
        log4 = Log(arg4, b1)
        
        
        
        prob_s = f"""
        Find
        $\\displaystyle  {log1}$
        """

        soln_s = f"""
        $\\displaystyle  {log1}$
        
        $\\displaystyle = {log2}$

        $\\displaystyle = {log3}$
        
        $\\displaystyle = {log4}$

        $\\displaystyle = {latex(answer)}$
        """

        answer_s = f"""
        $\\displaystyle {latex(answer)}$
        """
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass

    def problem_find_log_of_exp_product(self, b1, b1_expr, b2_expr, e1, e2, e3, e4):
        e_num = e1 * e3
        e_den = e2 * e4
        answer = e_num + e_den

        arg1_1 = ueval(ueval(b1) ** ueval(e1)) ** ueval(e3)
        arg1_2 = ueval(ueval(b1) ** ueval(e2)) ** ueval(e4)
        arg1 = arg1_1 * arg1_2

        num_latex1 = f"""\\left({b1_expr}\\right)^{e3} 
                        \\left({b2_expr}\\right)^{e4}"""

        log1 = Log(arg1, b1, f"\\left({num_latex1}\\right)", latex(b1))
        
        log2 = Log(arg1, b1)

        arg3_1 = ueval(b1) ** (ueval(e1) * ueval(e3))
        arg3_2 = ueval(b1) ** (ueval(e2) * ueval(e4))
        arg3 = arg3_1 * arg3_2
        
        log3 = Log(arg3, b1)
        
        arg4_1 = ueval(b1) ** (ueval(e1 * e3))
        arg4_2 = ueval(b1) ** (ueval(e2 * e4))
        arg4 = arg4_1 * arg4_2
        
        log4 = Log(arg4, b1)
        
        
        arg5 = ueval(b1) ** (ueval(e_num) + ueval(e_den))

        log5 = Log(arg5, b1)
        
        arg6 = ueval(b1) ** (ueval(answer))

        log6 = Log(arg6, b1)
        
        
        
        prob_s = f"""
        Find
        $\\displaystyle  {log1}$
        """

        soln_s = f"""
        $\\displaystyle  {log1}$
        
        $\\displaystyle = {log2}$

        $\\displaystyle = {log3}$
        
        $\\displaystyle = {log4}$

        $\\displaystyle = {log5}$

        $\\displaystyle = {log6}$

        $\\displaystyle = {latex(answer)}$
        """

        answer_s = f"""
        $\\displaystyle {latex(answer)}$
        """
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass

    def problem_find_log_of_div_exp_both(self, b1, b1_expr, b2_expr, e1, e2, e3, e4):
        e_num = e1 * e3
        e_den = e2 * e4
        answer = e_num - e_den

        num_latex1 = latex_fraction(f"\\left({b1_expr}\\right)^{e3}", 
                                   f"\\left({b2_expr}\\right)^{e4}")

        arg1_num = ueval(ueval(b1) ** ueval(e1)) ** ueval(e3)
        arg1_den = ueval(ueval(b1) ** ueval(e2)) ** ueval(e4)
        arg1 = arg1_num / arg1_den
        
        log1 = Log(arg1, b1, f"\\left({num_latex1}\\right)", latex(b1))

        num_latex2 = latex_fraction(f"{latex(arg1_num)}", 
                                   f"{latex(arg1_den)}")

        log2 = Log(arg1, b1, f"\\left({num_latex2}\\right)", latex(b1))


        arg3_num = ueval(b1) ** (ueval(e1) * ueval(e3))
        arg3_den = ueval(b1) ** (ueval(e2) * ueval(e4))

        num_latex3 = latex_fraction(f"{latex(arg3_num)}", 
                                   f"{latex(arg3_den)}")

        log3 = Log(arg1, b1, f"\\left({num_latex3}\\right)", latex(b1))
        
        arg4_num = ueval(b1) ** (ueval(e1 * e3))
        arg4_den = ueval(b1) ** (ueval(e2 * e4))

        num_latex4 = latex_fraction(f"{latex(arg4_num)}", 
                                   f"{latex(arg4_den)}")

        log4 = Log(arg1, b1, f"\\left({num_latex4}\\right)", latex(b1))
        

        arg5 = ueval(b1) ** (ueval(e_num) - ueval(e_den))

        log5 = Log(arg5, b1)
        
        arg6 = ueval(b1) ** (ueval(answer))

        log6 = Log(arg6, b1)
        
        prob_s = f"""
        Find
        $\\displaystyle  {log1}$
        """

        soln_s = f"""
        $\\displaystyle {log1}$
        
        $\\displaystyle = {log2}$
        
        $\\displaystyle = {log3}$
        
        $\\displaystyle = {log4}$

        $\\displaystyle = {log5}$

        $\\displaystyle = {log6}$

        $\\displaystyle = {latex(answer)}$
        """

        answer_s = f"""
        $\\displaystyle {latex(answer)}$
        """
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass

    def problem_log_reciprocal(self, a, b, x):
        
        logx_a = Log(x, a)
        ab_expr = ueval(a) ** ueval(b)
        
        logab_expr_a = Log(ab_expr, a)

        answer = 1

        prob_s = f"""
        Find value of $\\displaystyle {latex(x)} $ in 
        
        $\\displaystyle 
        {logx_a} = {latex(b)}
        $
        
        """

        soln_s = []

        soln_item = f"""
        $\\displaystyle 
        {logx_a} = {latex(b)}
        $
        
        $\\displaystyle \\implies
        {logx_a} = {logab_expr_a}
        $
        
        Cancel the log on both sides:
        
        $\\displaystyle  \\implies
        {latex(x)} = {latex(ab_expr)}
        $
        """
        soln_s.append(soln_item.strip())
        
        soln_s = "\n\n".join(soln_s)

        answer_s = f"""
        $\\displaystyle 
        {latex(ab_expr)}
        $
        """
        
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass


    def problem_log_chain_rule(self, a, b, c):
        if c == a:
            return self._problem_log_reciprocal(a, b)
        
        return self._problem_log_chain_rule(a, b, c)
        pass

    def _problem_log_chain_rule(self, a, b, c):
        x, y = symbols('x, y')

        logb_a = Log(b, a)
        logc_b = Log(c, b)
        bx_expr = ueval(a) ** ueval(x)
        cy_expr = ueval(a) ** ueval(y)

        logbx_expr_a = Log(bx_expr, a)
        logcy_expr_b = Log(cy_expr, bx_expr)

        cy_expr2 = ueval(ueval(a) ** x) ** (ueval(y) * ueval(1/x)) 
        logcy_expr2_b = Log(cy_expr2, bx_expr)
        
        y_by_x = ueval(y) * ueval(1/x)

        logay_a = Log(ueval(a) ** y, a)
        logc_a = Log(c, a)

        prob_s = f"""
        Simplify 
        
        $\\displaystyle 
        \\left({logb_a}\\right) 
        \\left({logc_b}\\right)
        $
        """

        soln_s = []

        soln_item = f"""
        We have to simplify: 
        
        $\\displaystyle 
        \\left({logb_a}\\right) 
        \\left({logc_b}\\right)
        $

        Let 
        $\\displaystyle 
        {latex(b)} = {latex(bx_expr)} 
        $
        $\\displaystyle 
        {latex(c)} = {latex(cy_expr)} 
        $
        
        Substituting values:
        
        $\\displaystyle 
        \\left({logbx_expr_a}\\right) 
        \\left({logcy_expr_b}\\right)
        $

        $\\displaystyle 
        = 
        \\left({logbx_expr_a}\\right) 
        \\left({logcy_expr2_b}\\right)
        $
        
        $\\displaystyle 
        =
        \\left({latex(x)}\\right) 
        \\left({latex(y_by_x)}\\right) 
        $
        
        $\\displaystyle 
        = {latex(y)}
        $
        
        $\\displaystyle 
        = {logay_a}
        $
        
        $\\displaystyle 
        = {logc_a} 
        $
        
        """
        soln_s.append(soln_item.strip())
        
        soln_s = "\n\n".join(soln_s)

        answer_s = f"""
        $\\displaystyle 
        {logc_a} 
        $
        """
        
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass

    def _problem_log_reciprocal(self, a, b):
        x, y = symbols('x, y')
        c = a
        y = 1 / x
        
        logb_a = Log(b, a)
        logc_b = Log(c, b)
        bx_expr = ueval(a) ** ueval(x)
        cy_expr = ueval(b) ** ueval(y)

        logbx_expr_a = Log(bx_expr, a)
        logcy_expr_b = Log(cy_expr, b)

        cy_expr2 = ueval(ueval(a) ** x) ** (ueval(y) * ueval(1/x)) 
        logcy_expr2_b = Log(cy_expr2, bx_expr)
        
        y_by_x = ueval(y) * ueval(1/x)

        logay_a = Log(ueval(a) ** y, a)
        logc_a = Log(c, a)

        answer = 1
        
        prob_s = f"""
        Simplify 
        
        $\\displaystyle 
        \\left({logb_a}\\right) 
        \\left({logc_b}\\right)
        $
        """

        soln_s = []

        soln_item = f"""
        We have to simplify: 
        
        $\\displaystyle 
        \\left({logb_a}\\right) 
        \\left({logc_b}\\right)
        $

        Let 
        $\\displaystyle 
        {latex(b)} = {latex(bx_expr)} 
        $
        $\\displaystyle \\implies
        {latex(c)} = {latex(cy_expr)} 
        $
        
        Substituting values:
        
        $\\displaystyle 
        \\left({logbx_expr_a}\\right) 
        \\left({logcy_expr_b}\\right)
        $

        $\\displaystyle 
        =
        \\left({latex(x)}\\right) 
        \\left({latex(y)}\\right) 
        $
        
        $\\displaystyle 
        = {latex(answer)}
        $
        """
        soln_s.append(soln_item.strip())
        
        soln_s = "\n\n".join(soln_s)

        answer_s = f"""
        $\\displaystyle 
        {latex(answer)}
        $
        """
        
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass


    def problem_product_of_two_terms(self, b, a1, a2):
        x, y = symbols('x, y')

        arg1 = ueval(a1) * ueval(a2)
        log1 = Log(arg1, b)

        bx = ueval(b) ** ueval(x)
        by = ueval(b) ** ueval(y)


        arg2 = ueval(bx) * ueval(by)
        log2 = Log(arg2, b)
        
        xy = ueval(x) + ueval(y)
        bxy = ueval(b) ** (xy)
        log3 = Log(bxy, b)

        log4_1 = Log(bx, b)
        log4_2 = Log(by, b)

        log5_1 = Log(a1, b)
        log5_2 = Log(a2, b)

        prob_s = f"""
        Simplify 
        
        $\\displaystyle 
        {log1} 
        $
        """

        soln_s = []

        soln_item = f"""
        We have to simplify: 
        
        $\\displaystyle 
        {log1} 
        $

        Let 
        $\\displaystyle 
        {latex(a1)} = {latex(bx)} 
        $
        $\\displaystyle 
        {latex(a2)} = {latex(by)} 
        $

        $\\displaystyle 
        {log1} 
        $

        $\\displaystyle 
        = {log2} 
        $
        
        $\\displaystyle 
        = {log3} 
        $
        
        $\\displaystyle 
        = {latex(xy)} 
        $
        
        $\\displaystyle 
        = {log4_1} + {log4_2} 
        $
        
        $\\displaystyle 
        = {log5_1} + {log5_2} 
        $

        Please note that product of two numbers results into addition of their logs.

        $\\displaystyle 
        {log1} = {log5_1} + {log5_2} 
        $
        
        """
        soln_s.append(soln_item.strip())
        
        soln_s = "\n\n".join(soln_s)

        answer_s = f"""
        $\\displaystyle 
        {log5_1} + {log5_2} 
        $
        """
        
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass


    def problem_div_of_two_terms(self, b, a1, a2):
        x, y = symbols('x, y')

        arg1 = ueval(a1) / ueval(a2)
        arg1_latex = latex_fraction(latex(ueval(a1)), latex(ueval(a2)))
        log1 = Log(arg1, b, f"\\left( {arg1_latex} \\right)")

        bx = ueval(b) ** ueval(x)
        by = ueval(b) ** ueval(y)


        arg2 = ueval(bx) / ueval(by)
        arg2_latex = latex_fraction(latex(ueval(bx)), latex(ueval(by)))
        log2 = Log(arg2, b, f"\\left( {arg2_latex} \\right)")
        
        xy = ueval(x) - ueval(y)
        bxy = ueval(b) ** (xy)
        log3 = Log(bxy, b)

        log4_1 = Log(bx, b)
        log4_2 = Log(by, b)

        log5_1 = Log(a1, b)
        log5_2 = Log(a2, b)

        prob_s = f"""
        Simplify 
        
        $\\displaystyle 
        {log1} 
        $
        """

        soln_s = []

        soln_item = f"""
        We have to simplify: 
        
        $\\displaystyle 
        {log1} 
        $

        Let 
        $\\displaystyle 
        {latex(a1)} = {latex(bx)} 
        $
        $\\displaystyle 
        {latex(a2)} = {latex(by)} 
        $

        $\\displaystyle 
        {log1} 
        $

        $\\displaystyle 
        = {log2} 
        $
        
        $\\displaystyle 
        = {log3} 
        $
        
        $\\displaystyle 
        = {latex(xy)} 
        $
        
        $\\displaystyle 
        = {log4_1} - {log4_2} 
        $
        
        $\\displaystyle 
        = {log5_1} - {log5_2} 
        $

        Please note that division of two numbers results into subtraction of divisor log from dividend log.
        
        $\\displaystyle 
        {log1} = {log5_1} - {log5_2} 
        $

        """
        soln_s.append(soln_item.strip())
        
        soln_s = "\n\n".join(soln_s)

        answer_s = f"""
        $\\displaystyle 
        {log5_1} - {log5_2} 
        $
        """
        
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass

    def problem_simplify_log_in_exponent(self, b, a1):
        x, y = symbols('x, y')

        log1 = Log(ueval(a1), ueval(b))
        logx = Log(ueval(x), ueval(b))

        exp1_latex = f"{{ {latex(b)} }}^{{ {log1} }}"
        log_exp1_latex = f"log_{{ {latex(b)} }}{{ \\left( {exp1_latex} \\right) }}"

        prob_s = f"""
        Simplify 
        
        $\\displaystyle 
        {exp1_latex} 
        $
        """

        soln_s = []

        soln_item = f"""
        Let
        $\\displaystyle 
        {latex(x)} = {exp1_latex} 
        $
        
        Apply log to both sides:

        $\\displaystyle \\implies
        {logx} = {log_exp1_latex} 
        $
        
        $\\displaystyle \\implies
        {logx} = {log1} 
        $
        
        $\\displaystyle \\implies
        {latex(x)} = {latex(a1)} 
        $
        
        $\\displaystyle \\implies
        {exp1_latex} = {latex(a1)} 
        $
        
        Please note that exponent and log are inverse of each other.
        
        """
        soln_s.append(soln_item.strip())
        
        soln_s = "\n\n".join(soln_s)

        answer_s = f"""
        $\\displaystyle 
        $
        """
        
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass


    def problem_log_of_multi_terms(self, b, n1, n2, n3, e1):
        x, y = symbols('x, y')
        
        data1 = []
        data2 = []
        
        data1.append([ueval(n1) * ueval(n2) ** ueval(e1), 
                     f"{Log(n1, b)} + {e1} {Log(n2, b)}"
                     ])

        data1.append([ueval(n1)** ueval(e1) * ueval(n2), 
                     f"{e1} {Log(n1, b)} + {Log(n2, b)}"
                     ])

        data1.append([ueval(n1) * ueval(n2), 
                     f"{Log(n1, b)} + {Log(n2, b)}"
                     ])

        data2.append([ueval(n3) ** ueval(e1), 
                     f"{e1} {Log(n3, b)}"
                     ])
        
        data2.append([ueval(n3), 
                     f"{Log(n3, b)}"
                     ])
        
        data = [random.choice(data1), random.choice(data2)]
        
        (a1, log_a1_latex), (a2, log_a2_latex) = random.sample(data, 2)

        arg1 = ueval(a1) / ueval(a2)
        arg1_latex = latex_fraction(latex(ueval(a1)), latex(ueval(a2)))
        log1 = Log(arg1, b, f"\\left( {arg1_latex} \\right)")

        prob_s = f"""
        Expand log 
        
        $\\displaystyle 
        {log1} 
        $
        """

        soln_s = []

        soln_item = f"""
        We have to simplify: 
        
        $\\displaystyle 
        {log1} 
        $

        {self._get_log_conversion_rule()}

        $\\displaystyle 
        {log1} 
        $

        $\\displaystyle 
        = \\left({log_a1_latex}\\right)  - \\left({log_a2_latex}\\right) 
        $
        
        """
        soln_s.append(soln_item.strip())
        
        soln_s = "\n\n".join(soln_s)

        answer_s = f"""
        $\\displaystyle 
        \\left({log_a1_latex}\\right)  - \\left({log_a2_latex}\\right) 
        $
        """
        
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass

    def problem_long_expanded_to_simplified(self, b, n1, n2, n3):
        count = random.randint(3, 10)
        
        temp = [n1, n2, n3]
        data = [n1, n2, n3]
        
        for i in range(1, count):
            try:
                random_idx = random.choice(list(range(len(data))))
                item = data[random_idx]
                temp_item = random.choice(temp)
                if item != temp_item:
                    temp_item = random.choice([temp_item, Rational(1, temp_item)])
                    
                mod_item = nsimplify(item * temp_item)
                
                if random.choice([True, False]):
                    data[random_idx] = mod_item
                else:
                    data.append(mod_item)
            except Exception as e:
                sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}: {e}
                """
                self._log(sError)
                self._log(traceback.format_exc())
                pass
            pass
        
        data = [(random.choice(['+', '-']), Log(item, b), item) 
                for item in data if item != 0]
        
        terms = 1
        terms_latex = []
        expanded_log_latex = []
        for op, term_log, term in data:
            term_latex = latex(term)

            if op == '+':
                if (isinstance(term, Symbol) or 
                    isinstance(term, float) or
                    isinstance(term, int) ):
                    term_latex = f"{term_latex}"
                else:
                    term_latex = f"\\left({term_latex}\\right)"
                    
                terms_latex.append(term_latex)
                terms = terms * term
            else:
                terms_latex.append(f"\\frac{{1}} {{{term_latex}}}")
                terms = terms / term
            
            if isinstance(term, Pow):
                mod_term, term_pow = term.args
                expanded_log_latex.append(f"{op} {term_pow} \\log_{{{b}}}{{{latex(mod_term)}}}" )
            else:
                expanded_log_latex.append(f"{op} \\log_{{{b}}} {term_latex}" )
            
            pass

        expanded_log_latex = "".join(expanded_log_latex)

        terms_latex = " * ".join(terms_latex)
        terms_latex = f"\\left( {terms_latex} \\right)"
        log_terms_latex = Log(terms, b, terms_latex)

        terms = nsimplify(terms)
        log_terms = Log(terms, b)
        
        
        prob_s = f"""
        Combine log 
        
        $\\displaystyle 
        {expanded_log_latex} 
        $
        """

        soln_s = []

        soln_item = f"""
        $\\displaystyle 
        {expanded_log_latex} 
        $

        As all logs have same base, we can combine them.
        {self._get_inverse_log_conversion_rule()}

        $\\displaystyle 
        {expanded_log_latex} 
        $

        $\\displaystyle 
        = {log_terms_latex} 
        $

        $\\displaystyle 
        = {log_terms} 
        $

        """
        soln_s.append(soln_item.strip())
        
        soln_s = "\n\n".join(soln_s)

        answer_s = f"""
        $\\displaystyle 
        {log_terms} 
        $
        """
        
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass


    def problem_custom_log_expr(self, expr):
        if expr is None:
            expr, expr_latex = self._get_random_expr()
            
        else:
            expr_latex = latex(expr)
        
        prob_s = f"""
        Solve: 
        $\\displaystyle 
        {expr_latex}
        $
        """
        
        soln_s = []
        
        soln_item = f"""
        $\\displaystyle 
        {expr_latex}
        $
        """
        soln_s.append(soln_item.strip())
        
        class_name = type(expr).__name__
        
        soln_item = f"""
        class_name = {class_name}
        """
        soln_s.append(soln_item.strip())
        
        
        answer_s = f"""
        $\\displaystyle 
        
        $
        """
        
        soln_s = "\n\n".join(soln_s)
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result
        pass

    def _init_default_symbs(self):
        symbs1 = symbols('b, b_1, b_2, b_3' )
        symbs2 = symbols('a, a_1, a_2, a_3, a_n' )
        symbs3 = symbols('x, y, z' )
        return (symbs1, symbs2, symbs3)

    def _get_random_expr(self):
        from sympy import log
        (symbs1, symbs2, symbs3) = self.default_symbs
        
        b, b1, b2, b3 = symbs1[:4]
        a, a1, a2, a3, an = symbs2[:5]
        x, y, z = symbs3[:3]
        
        exprs = []

        try:        
            expr = Log(a, b)
            exprs.append([expr, expr])
            
            expr = Log(a**2, b)
            exprs.append([expr, expr])

            log1 = Log(a, b)
            log2 = Log(a*2, b)
            log3 = Log(a*3, b)
            
            expr = ueval(log1) * ueval(log2) * ueval(log3)
            expr_latex = f"{log1} * {log2} * {log3}"
            exprs.append([expr, expr_latex])
            
            expr = ueval(log1) + ueval(log2) + ueval(log3)
            expr_latex = f"{log1} + {log2} + {log3}"
            exprs.append([expr, expr_latex])

            log1 = log(a, b)
            log2 = log(a, b) * 2
            log3 = log(a, b) * 3
            
            expr = ueval(log1) * ueval(log2) * ueval(log3)
            expr_latex = f"{latex(log1)} * {latex(log2)} * {latex(log3)}"
            exprs.append([expr, expr_latex])
            

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


    def _get_log_conversion_rule(self):
        s = f"""
        The expansion of logs follow these rules:
        Exponential into multiplication of log with the exponent.
        Division into subtraction of divisor log from dividend log.
        Multiplication into addition of their logs.
        """
        return s.strip()

    def _get_inverse_log_conversion_rule(self):
        s = f"""
        The combining of logs follows these rules:
        Multiplier of log is converted to exponent.
        Subtraction of logs: plus term is numerator and minus term denominator.
        Addition of logs: the terms are multipled.
        """
        return s.strip()

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


    def _plot_log_data(self, **kwargs):
        import numpy as np

        while True:
            nums_log = [1, 3, 6, 9, 12, 15]
            b = random.choice([2, 3, 5, 10])
            nums = [b**x for x in nums_log]
            
            if nums_log is None or nums is None:
                print("nums_log or nums is None. Continuing ...")
                continue
            
            break
            pass
        
        data = (nums_log, nums, b)
        return data
        pass
    
    
    def call_plot_method(self, method, *args, **kwargs):
        '''
        returns plot_code, svg_str
        
        @method: method to be called
        
        Pass these two methods to use cache util. Any other params passed will be subsequently 
        passed to the method to be called.
        @use_cache = True/False
        @refresh_cache = False/True
        '''
        from xv.util.data import CacheProcessor
        cache_processor = CacheProcessor(verbose = self.verbose)
        plot_code, svg_str = cache_processor.get_svg_and_runnable_code(method, 
                                                                       *args, 
                                                                       **kwargs)
        plot_code = plot_code.replace("self.verbose", "False")
        return plot_code, svg_str
        pass


    def _plot_log(self, data, **kwargs):
        
        import matplotlib.pyplot as plt
        import numpy as np

        nums_log, nums, b = data
        
        labels = [f"${{{b}}}^{{{x}}}$" for x in nums_log]
        log_labels = [f"$ log_{{{b}}} {{  {{{b}}}^{{{x}}} }} $" for x in nums_log]
        
        if nums_log is None or nums is None:
            print("nums_log or nums is None")
            return None, None
        
        xs = list(range(len(nums_log)))
        
        fig = plt.figure(figsize=(8, 4))
        gs = fig.add_gridspec(1, 2)
        
        row_id = 0
        col_id = 0
        ax = fig.add_subplot(gs[row_id, col_id])
        ys = nums
        ax.bar(labels, ys)
        ax.plot(xs, ys, color = 'red')
        ax.set_title(f" numbers")
        ax.set_xlim(min(xs) - 1, max(xs) + 1)
        ax.set_ylim(min(ys) - 1, max(ys) + 1)
        
        col_id = 1
        ax = fig.add_subplot(gs[row_id, col_id])
        ys = nums_log
        ax.bar(log_labels, ys)
        ax.plot(xs, ys, color = 'red')
        ax.set_title(f" logs of numbers")
        ax.set_xlim(min(xs) - 1, max(xs) + 1)
        ax.set_ylim(min(ys) - 1, max(ys) + 1)
        
        for label in ax.get_xticklabels():
            label.set_rotation(20)
            label.set_ha('right')
        
        fig.tight_layout()
        pass
    


    def problem_why_concept_of_log(self):
        data = self._plot_log_data()
        
        method_name = "_plot_log"
        
        #svg_str, plot_code, data = self._plot_specified_method(method_name)
        
        use_cache = False
        refresh_cache = False
        plot_code, svg_str = self.call_plot_method(self._plot_log,
                                                   data,
                                                   use_cache = use_cache,
                                                   refresh_cache = refresh_cache)
        
        
        nums_log, nums, b = data
        labels = [f"${{{b}}}^{{{x}}}$" for x in nums_log]
        
        
        prob_s = f"""
        Plot the number and its logs in two different charts.
        
        $\\displaystyle {list_to_str(nums)}$
        """
        
        soln_s = []
        
        soln_item = f"""
        $\\displaystyle {list_to_str(nums)}$
        
        = {list_to_str(labels)}
        
        Plot of $\\displaystyle {list_to_str(nums)}.
        $ Please note that the number are difficult to notice on the left plot. Log makes it easier to locate.
        {svg_str}
        """
        soln_s.append(soln_item.strip())
        
        
        soln_item = f"""
        <b>Code to plot:</b>
        <pre>{plot_code}</pre>
        """
        soln_s.append(soln_item.strip())
        
        answer_s = f"""
        """
        
        soln_s = "\n\n".join(soln_s)
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            'svg_str': svg_str,
            }
        
        return result


    def problem_log_of_common_numbers(self):
        
        a, x = symbols('a, x')
        
        nums_log = [ -1, -2, -3, 0, 1, 2, 3]
        nums_log = random.sample(nums_log, 4)
        b = random.choice([a, x, x**2, a + x, 2 * x, 
                           1/x, a/x, 
                           2, 3, 5, 10])
        
        #nums = [b**x for x in nums_log]
        
        nums_latex = [latex(b ** x) for x in nums_log]
        nums_latex = list_to_str(nums_latex)

        nums_pow_latex = [latex(ueval(b) ** ueval(x)) for x in nums_log]
        nums_pow_latex = list_to_str(nums_pow_latex)
        
        prob_s = f"""
        What are log values of the following numbers on base $\\displaystyle {latex(b)}$?
        
        $\\displaystyle {nums_latex}$
        """
        
        soln_s = []
        
        soln_item = f"""
        Log of $\\displaystyle {nums_latex}$ on base $\\displaystyle {latex(b)}$

        = Log of $\\displaystyle {nums_pow_latex}$ on base $\\displaystyle {latex(b)}$
        
        = {list_to_str(nums_log)}
        
        """
        soln_s.append(soln_item.strip())
        
        
        soln_item = f"""
        """
        soln_s.append(soln_item.strip())
        
        answer_s = f"""
        {list_to_str(nums_log)}
        """
        
        soln_s = "\n\n".join(soln_s)
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        
        return result



    def _plot_specified_method(self, method_name, **kwargs):
        plt.ioff()
        plotter = self
        
        if not hasattr(plotter, method_name):
            return None

        method = getattr(plotter, method_name)   
        plot_code = self._get_method_source_code(method)

        fig, ax, data = method(**kwargs)
        
        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, plot_code
            
        plt.clf()
        plt.close()
        return svg_str, plot_code, data
        
        
        
    def _get_method_source_code(self, method, start = 1, end = -2):
        '''
        method to call. It must be a method, not name
        start: -1, line index to start
        end: -2, line index to end
        '''
        plot_code = get_function_source_code(method, 
                                             strip_tab = False)
        plot_code = plot_code[start : end]
        plot_code = strip_source_code_tabs(plot_code)
        
        plot_code.append("plt.show()")
        
        plot_code = "\n".join(plot_code)
        return plot_code
        pass

    pass
#class LogarithmHelper:
