'''
Created on 12-Jul-2021

@author: bthakur

'''
import inspect
import traceback

import random
from xv.km.util import latex_fraction
from xv.util import toIntOrFloat

from sympy import (symbols, Symbol, limit, latex, Function,
                   Limit, oo, Derivative, simplify,
                   exp, log, expand_log,
                   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)

import sympy as sp

from xv.km.util import latex_fraction, latex_color

from .._algebra import PolynomialHelper

# _differentiation_theory_helper
# DifferentiationTheoryHelper

from xv.util import UtilClass

class DifferentiationTheoryHelper(UtilClass):
    
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.x = Symbol('x')
        self.n = Symbol('n', constant = True, real = True)
        self.k = Symbol('k', real = True)

        self.h = Symbol(f'\\Delta {self.x}')
        self.f = Function('f')(self.x)
        self.g = Function('g')(self.x)
        pass

    def get_function_delta(self, f, x, x0, x1):
        delta = Symbol(rf'\Delta')
        
        f0 = f.subs(x, x0)
        f1 = f.subs(x, ueval(x1))
        
        f1_latex = latex_color(f"{latex(f1)}", 'red')
        f0_latex = latex_color(f"{latex(f0)}", 'green')
        
        f_delta = simplify(f1  - f0)
        f_delta_latex = f"\left( {f1_latex} \\right) - \\left( {f0_latex} \\right)"
    
        
        soln_s = f"""$\\displaystyle
        \\begin{{aligned}}
        & {latex(delta)} \\left( {latex(f)} \\right)
        \\\\ \\\\
        
        & = {f_delta_latex}
        \\\\ \\\\
        
        & = {latex(f_delta)}
        
        \\end{{aligned}}
        $
        """

        answer_s = f"""$\\displaystyle
        {latex(f_delta)}
        $
        """
        
        result = {
            'f': f,
            'x': x,
            'h': ueval(x1) - ueval(x0),
            'x0': x0,
            'x1': x1,
            'f_delta': f_delta,
            'f_delta_latex': f_delta_latex,
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        return result
        pass

    
    def define_limit(self, f, x, x0):
        h = Symbol(f'\\Delta {latex(x)}')
        
        fx0 = f.subs(x, x0)
        
        
        limit_fx0 = f"""
        \\lim_{{{latex(h)} \\to {latex(x0)}}}
        \\left(
        {latex(f)} 
        \\right)
        """

        soln_items = []
        soln_item = f"""
        $\\displaystyle
        {limit_fx0} 
        $
        """
        soln_items.append(soln_item.strip())

        soln_s = "\n\n".join(soln_items)
        
        answer_s = soln_s
        
        
        result = {
            'f': f,
            'x': x,
            'h': h,
            'fx0': fx0,
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        return result
        pass


    def define_continuity(self, f, x, x0):
        h = Symbol(f'\\Delta {latex(x)}')
        
        fx0 = f.subs(x, x0)
        fx0_minus_h = f.subs(x, x0 - ueval(h))
        fx0_plus_h = f.subs(x, x0 + ueval(h))
        
        limit_fx0_minus_h = Limit(fx0_minus_h, h, 0)
        limit_fx0_plus_h = Limit(fx0_plus_h, h, 0)
        
        
        limit_fx0_minus_h_latex = f"""
        \\lim_{{{latex(h)} \\to 0^+}}
        \\left(
        {latex(fx0_minus_h)} 
        \\right)
        """
        
        limit_fx0_plus_h_latex = f"""
        \\lim_{{{latex(h)} \\to 0^+}}
        \\left(
        {latex(fx0_plus_h)} 
        \\right)
        """

        soln_items = []
        soln_item = f"""
        $\\displaystyle
        {limit_fx0_minus_h_latex} 
        = {latex(fx0)} 
        = {limit_fx0_plus_h_latex} 
        $
        """
        soln_items.append(soln_item.strip())

        soln_s = "\n\n".join(soln_items)
        
        answer_s = soln_s
        
        
        result = {
            'f': f,
            'x': x,
            'h': h,
            'xh': x + ueval(h), 
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        return result
        pass

    
    def define_derivative(self, f, x):
        h = Symbol(f'\\Delta {latex(x)}')
        
        
        df_exprs = self.get_derivative_exprs(f, x, h)
        
        
        
        soln_s = f""" 
        $\\displaystyle
        {latex(Derivative(f, x))}
        $
        
        $\\displaystyle
        = {df_exprs['def_latex']}
        $
        
        $\\displaystyle
        = {df_exprs['df_dx_latex']}
        $
        
        """
    
        answer_s = f"""
        $\\displaystyle
        {df_exprs['df_dx_latex']}
        $
        """
        
        result = {
            'f': f,
            'x': x,
            'h': h,
            'xh': x + ueval(h), 
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        result.update(df_exprs)
        
        return result
        pass
   
    def get_derivative_rule_add(self, f, g, x, h):
        
        dfg_exprs = self.get_derivative_exprs(f + g, x, h)
        df_exprs = self.get_derivative_exprs(f, x, h)
        dg_exprs = self.get_derivative_exprs(g, x, h)
        
        
        soln_items = []
        
        soln_item = f"""
        $\\displaystyle
        {latex(Derivative(ueval(f + g), x))}
        $
        
        $\\displaystyle
        = {dfg_exprs['def_latex']}
        $
        
        $\\displaystyle
        = {dfg_exprs['df_dx_latex']}
        $
        """
        soln_items.append(soln_item.strip())
        
        soln_item = f"""
        $\\displaystyle
        = {df_exprs['df_dx_latex']} + {dg_exprs['df_dx_latex']}
        $
        
        $\\displaystyle
        = {latex(Derivative(f, x))} + {latex(Derivative(g, x))}
        $
        """
        soln_items.append(soln_item.strip())

        soln_s = "\n\n".join(soln_items)
        
        answer_s = f"""
        $\\displaystyle
        {latex(Derivative(f, x))} + {latex(Derivative(g, x))}
        $
        """
        
        
        result = {
            'f': f,
            'g': g,
            'x': x,
            'h': h,
            'xh': x + ueval(h), 
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        result.update(dfg_exprs)
        
        return result
        pass
    
    def get_derivative_rule_mul(self, f, g, x, h):
        
        dfg_exprs = self.get_derivative_exprs(f * g, x, h)
        df_exprs = self.get_derivative_exprs(f, x, h)
        dg_exprs = self.get_derivative_exprs(g, x, h)
        
        
        soln_items = []
        
        soln_item = f"""
        $\\displaystyle
        {latex(Derivative(f * g, x))}
        $
        
        $\\displaystyle
        = {dfg_exprs['def_latex']}
        $
        
        $\\displaystyle
        = {dfg_exprs['df_dx_latex']}
        $
        """
        soln_items.append(soln_item.strip())

        xh = ueval(x) + h
        fxh = f.subs(x, xh)
        gxh = g.subs(x, xh)
        
        t1 = latex_color(f"\\left( {latex(fxh)} {latex(gxh)} \\right)", 'red')
        t2 = latex_color(f"\\left( {latex(f)} {latex(gxh)} \\right)", 'green')
        t3 = latex_color(f"\\left( {latex(f)} {latex(g)} \\right)", 'blue')
        
        
        
        
        numr = f""" {t1} - {t2} + {t2} - {t3} """
        denr = f""" {latex(h)} """

        item_fraction = latex_fraction(numr, denr)
        
        soln_item = f"""
        $\\displaystyle
        = \\lim_{{{latex(h)} \\to 0^+}}
        \\left( {item_fraction} \\right)
        $
        """
        soln_items.append(soln_item.strip())
        
        numr1 = f""" {t1} - {t2} """
        numr2 = f""" {t2} - {t3} """

        item_fraction1 = latex_fraction(numr1, denr)
        item_fraction2 = latex_fraction(numr2, denr)
        
        soln_item = f"""
        $\\displaystyle
        = \\lim_{{{latex(h)} \\to 0^+}}
        \\left( {item_fraction1} \\right)
        + 
         \\lim_{{{latex(h)} \\to 0^+}}
        \\left( {item_fraction2} \\right)
        $
        """
        soln_items.append(soln_item.strip())
        
        soln_item = f"""
        $\\displaystyle
        =  {df_exprs['df_dx_latex']} 
        * \\lim_{{{latex(h)} \\to 0^+}} {{ {latex(gxh)} }}
        + {{ {latex(f)} }} 
        * {dg_exprs['df_dx_latex']}
        $
        
        """
        soln_items.append(soln_item.strip())
        
        
        soln_item = f"""
        $\\displaystyle
        =  {df_exprs['df_dx_latex']} * {latex(g)} 
        + {latex(f)} * {dg_exprs['df_dx_latex']}
        $
        
        $\\displaystyle
        = \\left({latex(Derivative(f, x))} \\right) {latex(g)} 
        + {latex(f)} \\left({latex(Derivative(g, x))} \\right)
        $
        """
        soln_items.append(soln_item.strip())
        
        soln_s = "\n\n".join(soln_items)

        answer_s = f"""
        $\\displaystyle
         \\left({latex(Derivative(f, x))} \\right) {latex(g)} 
        + {latex(f)} \\left({latex(Derivative(g, x))} \\right)
        $
        """
        
        
        result = {
            'f': f,
            'g': g,
            'x': x,
            'h': h,
            'xh': x + ueval(h), 
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        result.update(dfg_exprs)
        
        return result
        pass
    
    def compute_derivative_x_power_n(self):
        ltx = self._modified_latex
        
        x = self.x
        n = self.n
        
        n = random.choice([n, n * random.randint(5, 9), random.randint(1, 9)])
        
        f = x ** n
        h = Symbol(f'\\Delta {latex(x)}')
        k = self.k
        
        
        df_exprs = self.get_derivative_exprs(f, x, h)
        
        
        soln_items = []
        
        soln_item = f"""
        $\\displaystyle
        {ltx(Derivative(f, x))}
        $
        
        $\\displaystyle
        = {df_exprs['def_latex']}
        $
        
        $\\displaystyle
        = {df_exprs['df_dx_latex']}
        $
        
        """
        soln_items.append(soln_item.strip())
        
        fx_plus_h_sum = Sum(ueval(binomial(n, k)) * ueval(x**k) * ueval(h**(n-ueval(k))), (k, n, 0))
        
        t1 = latex_color(f""" \\left( {latex(fx_plus_h_sum)} \\right) """, 'red')
        t2 = latex_color(f""" \\left( {latex(f)} \\right) """, 'blue')

        numr = f""" {t1} - {t2} """
        denr = f""" {latex(h)} """

        item_fraction = latex_fraction(numr, denr)
        
        soln_item = f"""
        Expand $\\displaystyle {latex(df_exprs['fxh'])}$ and simplify:

        $\\displaystyle
        = \\lim_{{{latex(h)} \\to 0^+}} 
        {{ {item_fraction} }}
        $
        """
        soln_items.append(soln_item.strip())
        
        #Sum(ueval(binomial(n, k)) * ueval(x**k) * ueval(h**(n-ueval(k))), (k, n, 0))
        expanded_terms = []
        max_idx = 4
        for idx in range(0, max_idx + 1):
            term = ueval(binomial(n, n - idx)) * ueval(x** (n - idx)) * h** ueval(idx)
            expanded_terms.append(term)
            pass
        
        expanded_terms_latex = [latex(term) for term in expanded_terms]
        
        expanded_terms_latex = " + ".join(expanded_terms_latex)

        t1 = latex_color(f""" \\left( {expanded_terms_latex} + \\cdots \\right) """, 'red')

        numr = f""" {t1} - {t2} """

        item_fraction = latex_fraction(numr, denr)
        
               
        soln_item = f"""
        $\\displaystyle
        = \\lim_{{{latex(h)} \\to 0^+}} 
        {{ {item_fraction} }}
        $
        """
        soln_items.append(soln_item.strip())
        
        modified_expanded_terms = expanded_terms.copy()
        modified_expanded_terms[0] = f
        modified_expanded_terms[1] = n * ueval(x ** (n-1)) * ueval(h)

        expanded_terms_latex = [latex(term) for term in modified_expanded_terms]
        expanded_terms_latex = " + ".join(expanded_terms_latex)
        t1 = latex_color(f""" \\left( {expanded_terms_latex} + \\cdots \\right) """, 'red')
        numr = f""" {t1} - {t2} """
        item_fraction = latex_fraction(numr, denr)
               
        soln_item = f"""
        $\\displaystyle
        = \\lim_{{{latex(h)} \\to 0^+}} 
        {{ {item_fraction} }}
        $
        """
        soln_items.append(soln_item.strip())
        
        expanded_terms_latex = [latex(term) for term in modified_expanded_terms[1:]]
        expanded_terms_latex = " + ".join(expanded_terms_latex)
        t1 = latex_color(f""" \\left( {expanded_terms_latex} + \\cdots \\right) """, 'red')
        numr = f""" {t1}"""
        item_fraction = latex_fraction(numr, denr)
               
        soln_item = f"""
        Cancel both $\\displaystyle {latex(f)}$ terms:

        $\\displaystyle
        = \\lim_{{{latex(h)} \\to 0^+}} 
        {{ {item_fraction} }}
        $
        """
        soln_items.append(soln_item.strip())


        expanded_terms_latex = [latex(term / h) for term in modified_expanded_terms[1:]]
        expanded_terms_latex = " + ".join(expanded_terms_latex)
        t1 = latex_color(f""" \\left( {expanded_terms_latex} + \\cdots \\right) """, 'red')

        soln_item = f"""
        Divide each term in numerator with denominator $\\displaystyle {latex(h)}$:
        
        $\\displaystyle
        = \\lim_{{{latex(h)} \\to 0^+}} 
        {{ {t1} }}
        $
        """
        soln_items.append(soln_item.strip())

        df_dx_doit = n * x ** (n-1)
        
        soln_item = f"""
        Replace $\\displaystyle {latex(h)} = 0 $:
        
        $\\displaystyle
        = {latex(df_dx_doit)}
        $
        """
        soln_items.append(soln_item.strip())
        
        
        soln_s = "\n\n".join(soln_items)

    
        answer_s = f"""
        $\\displaystyle
        {latex(df_dx_doit)}
        $
        """
        
        result = {
            'f': f,
            'x': x,
            'h': h,
            'xh': x + ueval(h), 
            'df_dx_doit': df_dx_doit,
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        result.update(df_exprs)
        
        return result
        pass
   
    def get_derivative_exprs(self, f, x, h):
        fxh = f.subs(x, x + ueval(h))
        
        fxh_latex = latex_color(latex(fxh), 'red')
        f_latex = latex_color(latex(f), 'blue')

        if not isinstance(f, Function):
            fxh_latex = f""" \\left( {fxh_latex}  \\right) """
            f_latex = f""" \\left( {f_latex}  \\right) """
        
        xh_latex = latex_color(latex(x + ueval(h)), 'red')
        h_latex = latex(ueval(h))

        
        def_latex_fraction = latex_fraction(f""" \\Delta \\left( {latex(f)} \\right) """,
                                           f""" {h_latex} """)
        
        def_latex = f"""
        \\lim_{{{latex(h)} \\to 0^+}}
        \\left( {def_latex_fraction} \\right)
        """


        df_dx_expr = (ueval(fxh) - ueval(f)) / h 
        
        df_dx_expr_latex = latex_fraction(f""" {fxh_latex} - {f_latex} """,
                                          f""" {h_latex} """)
            
        df_dx_latex = f"""
        \\lim_{{{latex(h)} \\to 0^+}}
        \\left( {df_dx_expr_latex} \\right)
        """

        result = {
            'fxh': fxh,
            'df_dx_expr': df_dx_expr,
            'df_dx_expr_latex': df_dx_expr_latex,
            'def_latex': def_latex,
            'df_dx_latex': df_dx_latex,
            }
        
        return result
        
    
    def _parse_fxh(self, fxh):
        if not fxh:
            return (self.f, self.x, self.h)
        
        if len(fxh) == 2:
            f = fxh[0]
            x = fxh[1]
            h = Symbol(rf'\Delta {x}') 
            return (f, x, h)
        
        if len(fxh) == 1:
            f = fxh[0]
            x = f.free_symbols[0]
            h = Symbol(rf'\Delta {x}') 
            return (f, x, h)
        
        return fxh
    
    def get_random_functions(self, size = 2):
   
        x = Symbol('x')
        x0 = Symbol('x_0')

        funcs = []
        for func_symb in list('fghmnp'):
            func = Function(func_symb)
            func = func(x)
            funcs.append(func)
            if len(funcs) == size:
                break
        return funcs, x, x0

   
    def get_random_function(self):
        f = Function('f')
        x = Symbol('x')
        n = Symbol('n')
        x0 = Symbol('x_0')
        f = f(x)
        return f, x, x0

    


    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 not hasattr(expr, 'subs'):
            return expr

        if funct_symbs is None:
            return expr

        if not funct_symbs:
            return expr
        
        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_standard_exprs(self, x = None):
        if x is None:
            x = self.x

        exprs = []
        
        exprs.extend(self.get_standard_misc_exprs())
        exprs.extend(self.get_standard_algebraic_exprs())
        exprs.extend(self.get_standard_trig_exprs())
        
        return exprs
        pass
            
    def get_standard_misc_exprs(self, x = None):
        if x is None:
            x = self.x
            
        f = self.f
        g = self.g

        u = Function('u')(x)
        v = Function('v')(x)

        a, b, c = symbols('a, b, c', real = True)

        exprs = []
        exprs.append(sp.sympify(random.randint(-9, 9)))
        
        exprs.append(f)
        exprs.append(g)
        exprs.append(u + v)
        exprs.append(u - v)
        exprs.append(u * v)
        exprs.append(u / v)
        
        return exprs
        pass


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

        a, b, c, n = symbols('a, b, c, n', real = True)

        exprs = []
        exprs.append(x ** n)
        exprs.append(x ** (1 / n))
        exprs.append(x ** (-n))
        exprs.append(x ** (-1))
        exprs.append(sp.sqrt(x))
        exprs.append(1 / sp.sqrt(x))
        
        exprs.append(sp.log(x))
        exprs.append(sp.log(x, a))
        
        exprs.append(a * x + b)
        exprs.append(a * x ** 2 + b * x + c)

        exprs.append(a ** x)
        exprs.append(sp.E ** x)
        
        return exprs
        pass


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

        exprs = []
        exprs.append(sp.sin(x))
        exprs.append(sp.cos(x))
        exprs.append(sp.tan(x))
        exprs.append(sp.csc(x))
        exprs.append(sp.sec(x))
        exprs.append(sp.cot(x))
        
        
        exprs.append(sp.asin(x))
        exprs.append(sp.acos(x))
        exprs.append(sp.atan(x))
        exprs.append(sp.acsc(x))
        exprs.append(sp.asec(x))
        exprs.append(sp.acot(x))
        
        exprs.append(sp.sinh(x))
        exprs.append(sp.cosh(x))
        exprs.append(sp.tanh(x))
        exprs.append(sp.csch(x))
        exprs.append(sp.sech(x))
        exprs.append(sp.coth(x))
        
        
        exprs.append(sp.asinh(x))
        exprs.append(sp.acosh(x))
        exprs.append(sp.atanh(x))
        exprs.append(sp.acsch(x))
        exprs.append(sp.asech(x))
        exprs.append(sp.acoth(x))
        
        return exprs
        pass

            
    pass
#class DifferentiationTheoryHelper:
