'''
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 sympy

from sympy import (symbols, Symbol, latex, Function,
                   Limit, oo, Derivative, diff, simplify,
                   sin, cos, tan, exp, log, sqrt,
                   Mul, Add, Pow
                   )

from xv.km.util import latex_fraction
from xv.util import run_with_timeout

from xv.util import UtilClass

class LHopitalsHelper(UtilClass):
    '''
    Calculate Limit in calculus with L H'opitals
    Example:
        from xv.math.calculus.helper import LimitHelper
        from xv.math.calculus.helper import LHopitalsHelper
        from sympy import *
        
        lHopitalsHelper = LHopitalsHelper(verbose = True)
        g, h, x, x0 = lHopitalsHelper.get_random_expr(advanced = True)
        
        limitHelper = LimitHelper(verbose = True)
        result = limitHelper.get_solution(g/h, x, x0)
        result_str = limitHelper.solution_to_str(result)
        
    doc_style: xv_doc
    '''    
    
    _MAX_FUNC_LENGTH = 150
    _LABEL = "L"

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

    def _init_solution_config(self, solution_idx = 0):
        self._solution_idx = solution_idx
        self._tried_steps = []
        self._counter = 0
        self._solution_found = False

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

    def get_solution(self, f, x, x0, 
                     solution_idx = 0, 
                     parent_label = None):
        
        if not self._is_lhopital_expr(f, x, x0):
            self._log(f"{f} is not a LHopitals expr")
            return None
        
        self._log(f"Call LHopitals rule: solution_idx = {solution_idx}")
        self._init_solution_config(solution_idx = solution_idx)

        solution_path = self._init_solution_path(f, x, x0)
        if not solution_path:
            self._log(f"\nNo valid solution_path")
            return None
        
        self._log(f"\nStarting to process solution_path = \n{solution_path}")
        solution = self._get_solution_step(solution_path,
                                           parent_label = parent_label)
        return solution
        pass
    
    def _get_solution_step(self, solution_path, 
                           parent_label = None,
                           has_lHopital_parent = False
                           ):
        solution_idx = self._next_solution_idx() 
        
        label = Symbol(f"{LHopitalsHelper._LABEL}_{{{solution_idx}}}")
        if parent_label is None:
            parent_label = Symbol(f"{LHopitalsHelper._LABEL}_{{{solution_idx-1}}}")

        self._log(f"\n{solution_idx}. solution_path = \n{solution_path}")
        
        g = solution_path.get('g', None)
        h = solution_path.get('h', None)
        x = solution_path.get('x', None)
        x0 = solution_path.get('x0', None)
        f = simplify(g/h)

        f_Lim = Limit(f, x, x0)
        answer  = self._get_limit(f, x, x0)

        soln_statement = []
        
        g_limit = self._get_limit(g, x, x0)
        h_limit = self._get_limit(h, x, x0)
        if ((abs(g_limit) == oo and abs(h_limit) == oo) or 
            (abs(g_limit) == 0 and abs(h_limit) == 0)):
            
            return self._get_solution_step_l_hospital(solution_path, 
                                               parent_label = parent_label, 
                                               has_lHopital_parent = has_lHopital_parent)
            pass

        sOut = f"""
        ## THAKUR
        False signal for LHopitals rule:
        x0 = {x0}
        g_limit = {g} = {g_limit}
        h_limit = {h} = {h_limit}
        """
        #soln_statement.append(sOut.strip())
        
        if simplify(answer) != simplify(g_limit / h_limit):
            sOut = f"""
            ## THAKUR WRONG ANSWER
            The solution is wrong as:
            answer = {answer} != g_limit / h_limit = {g_limit / h_limit}
            
            g_limit = {g} = {g_limit}
            h_limit = {h} = {h_limit}
            """
            soln_statement.append(sOut.strip())
            
            pass
        

        applicable_statement = self.get_applicable_statement(g, h, x, x0,
                                    parent_label,
                                    has_lHopital_parent = has_lHopital_parent,
                                    is_lHopital = False)
        

        if not has_lHopital_parent:
            start_statement = applicable_statement
        else:
            start_statement = applicable_statement

        soln_statement = "\n\n".join(soln_statement)
    
        solution_step = {
            'solution_idx': solution_idx,
            'method_name': inspect.stack()[0][3],
            'f': f,
            'x': x,
            'x0': x0,
            'Limit': f_Lim,
            'type': 'LHopitals',
            'start_statement': start_statement,
            'statement': soln_statement,
            'post_statement': None,
            'answer': answer,
            'children': None
            }
        
        return solution_step
        pass

    def _get_solution_step_l_hospital(self, solution_path, 
                           parent_label = None,
                           has_lHopital_parent = False
                           ):
        solution_idx = self._next_solution_idx() 
        
        label = Symbol(f"{LHopitalsHelper._LABEL}_{{{solution_idx}}}")
        if parent_label is None:
            parent_label = Symbol(f"{LHopitalsHelper._LABEL}_{{{solution_idx-1}}}")

        self._log(f"\n{solution_idx}. solution_path = \n{solution_path}")
        
        g = solution_path.get('g', None)
        h = solution_path.get('h', None)
        x = solution_path.get('x', None)
        x0 = solution_path.get('x0', None)
        f = simplify(g/h)
        
        f_Lim = Limit(f, x, x0)
        
        
        soln_statement = []
        applicable_statement = self.get_applicable_statement(g, h, x, x0,
                                    parent_label,
                                    has_lHopital_parent = has_lHopital_parent)
        

        lmt_stmt = f"\\lim _{{x \\rightarrow {latex(x0)}}}"
        
        f_frac = latex_fraction(latex(g), 
                                latex(h))
        g_D = Derivative(g, x)
        h_D = Derivative(h, x)
        f_D_frac = latex_fraction(latex(g_D), latex(h_D))
        
        g_diff = g_D.doit()
        h_diff = h_D.doit()
        f_diff = g_diff / h_diff
        f_diff_simpliefied = simplify(f_diff)
        f_diff_frac = latex_fraction(latex(g_diff), latex(h_diff))
        
        
        numr_Limit = Limit(g_diff, x, x0)
        denr_Limit = Limit(h_diff, x, x0)
        
        f_diff_Limit_frac = latex_fraction(latex(numr_Limit), latex(denr_Limit))
        
        numr = self._get_limit(g_diff, x, x0)
        denr = self._get_limit(h_diff, x, x0)
        
        if numr is None or denr is None:
            sError = f"""Error: {self.__class__.__name__}.{inspect.stack()[0][3]}:
            Failed to get limit of g_diff or h_diff
            g_diff = {g_diff}
            h_diff = {h_diff}
            x = {x}
            x0 = {x0}
            """
            self._log(sError)
            return None 
        
        f_diff_lim_frac = latex_fraction(latex(numr), latex(denr))
        
        
        answer  = f_Lim.doit()
        
        child_solutions = {}
        
        
        child_solution_path = solution_path.get('child_solution_path', None)
        if child_solution_path:
            child_solution = self._get_solution_step(child_solution_path,
                                            parent_label = label,
                                            has_lHopital_parent = True)
            if child_solution:
                child_solutions['LHopitals'] = child_solution
                answer = child_solution['answer']
            pass
        
        if not has_lHopital_parent:
            start_statement = applicable_statement
        else:
            start_statement = applicable_statement
            #start_statement = f"<b>Apply L’Hopital’s Rule again.</b>"
            
        
        statement = f"""
        $\\displaystyle {latex(parent_label)} = {latex(f_Lim)}$

        $\\displaystyle = {lmt_stmt} \\left( {f_frac} \\right) $
        
        $\\displaystyle = {lmt_stmt}  \\left( {f_D_frac} \\right) $
        
        $\\displaystyle = {lmt_stmt}  \\left( {f_diff_frac} \\right)
         = {latex(label)} 
        $
        
        """
        soln_statement.append(statement.strip())

        #if this is final lHopital loop        
        if not child_solution_path:
            statement = f"""
            $\\displaystyle 
            \\implies {latex(label)}
            = {f_diff_Limit_frac} 
            $
            
            $\\displaystyle 
            \\implies {latex(label)} 
            = {f_diff_lim_frac}$

            $\\displaystyle 
            \\implies {latex(label)} 
            = {latex(answer)}$


            $\\displaystyle 
            \\implies {latex(parent_label)}
            = {latex(answer)} \\qquad \\text{{as}}\\
            {latex(parent_label)} = {latex(label)}
            $
            """
            soln_statement.append(statement.strip())
            
            post_statement = ""
            pass
        else:
            statement = f"""
            $\\displaystyle 
            \\implies {latex(label)}
            = {lmt_stmt}  \\left( {latex(f_diff_simpliefied)} \\right) 
            $
            """
            soln_statement.append(statement.strip())
        
            post_statement = f"""
            $\\displaystyle 
            \\implies {latex(parent_label)}
            = {latex(answer)} \\qquad \\text{{as}}\\
            {latex(parent_label)} = {latex(label)}
            $
            """
            pass

        soln_statement = "\n\n".join(soln_statement)
        
        solution_step = {
            'solution_idx': solution_idx,
            'method_name': inspect.stack()[0][3],
            'f': f,
            'x': x,
            'x0': x0,
            'Limit': f_Lim,
            'type': 'LHopitals',
            'start_statement': start_statement,
            'statement': soln_statement,
            'post_statement': post_statement,
            'answer': answer,
            'children': child_solutions
            }
        
        return solution_step
        pass



    def _init_solution_path(self, f, x, x0, max_recursion = 5):
        func_sets = self._possible_func_sets(f, x, x0)
        #func_sets = random.sample(func_sets, len(func_sets))
        
        func_sets_str = [f"{t}" for t in  func_sets]
        func_sets_str = "\n\n".join(func_sets_str)
        self._log(f"""
func_sets of {f}

------------------
{func_sets_str}
        """)
        
        for func_set_idx, func_set in enumerate(func_sets):
            g, h = func_set
            init_solution_path = {'g': g, 'h': h, 'x': x, 'x0': x0}
            
            self._log(f"\n{func_set_idx}/{len(func_sets)}.{max_recursion}. checking func_set = {init_solution_path}")
            solution_path = self._get_possible_solution_path(
                                    init_solution_path, 
                                    max_recursion = max_recursion)
            if solution_path:
                return solution_path
            pass
        return None
        pass
    
    def _get_possible_solution_path(self, solution_path, max_recursion = 2):
        self._log(f"max_recursion = {max_recursion}. checking solution_path = \n{solution_path}")
        
        if self._solution_found:
            return solution_path
        
        g = solution_path.get('g', None)
        h = solution_path.get('h', None)
        x = solution_path.get('x', None)
        x0 = solution_path.get('x0', None)
        if None in [g, h, x, x0]:
            self._log(f"g = {g} or h = {h} or x = {x} or x0 = {x0} None.")
            return None
        
        len_g = len(f"{g}")
        len_h = len(f"{h}")
        if (len_g > LHopitalsHelper._MAX_FUNC_LENGTH or 
            len_h > LHopitalsHelper._MAX_FUNC_LENGTH):
            self._log(f"""
            Returning as functions are too long to continue: 
            g = {len_g}, h = {len_h}
            max length allowed: {LHopitalsHelper._MAX_FUNC_LENGTH}
            """)
            return None
            pass
        
        g_diff_limit = self._get_limit(diff(g, x), x, x0)
        h_diff_limit = self._get_limit(diff(h, x), x, x0)
        
        if g_diff_limit is None or h_diff_limit is None:
            return None

        if not self._is_lhopital_pair(g_diff_limit, h_diff_limit):
            self._solution_found = True

            sOut = f"""
            Returning:
            {solution_path}
                not _is_lhopital_pair:
                g_diff_limit = {g_diff_limit}
                h_diff_limit = {h_diff_limit}
            """
            
            solution_path['g_diff_limit'] = g_diff_limit
            solution_path['h_diff_limit'] = h_diff_limit
            solution_path['valid_answer'] = True
            solution_path['answer'] = g_diff_limit/h_diff_limit
            
            return solution_path
            pass

        self._log(f"""
            lhopital_pair, to process further:
            g_diff_limit = {g_diff_limit} = {diff(g, x)}
            h_diff_limit = {h_diff_limit} = {diff(h, x)}
        """)
        if max_recursion <= 0:
            self._log(f"max_recursion = {max_recursion}. Returning None.")
            return None
        
        func_sets = self._possible_func_sets(
                                diff(g, x)/diff(h, x), x, x0)
        if not func_sets:
            self._log(f"\nFailed to get func_sets. Returning None.")
            return None
        
        func_sets_str = [f"{t}" for t in  func_sets]
        func_sets_str = "\n\n".join(func_sets_str)
        self._log(f"""
func_sets of {diff(g, x)/diff(h, x)}

------------------
{func_sets_str}
        """)
        self._log(f"\nProcessing {len(func_sets)} func_sets")
        for func_set_idx, func_set in enumerate(func_sets):
            poss_solution_path = {'g': func_set[0], 'h': func_set[1], 
                                  'x': x, 'x0': x0}
            self._log(f"\n{func_set_idx}/{len(func_sets)}.{max_recursion - 1}. checking func_set = {poss_solution_path}")
            
            '''
            name = f"{g}/{h}"
            timeout = 10
            child_solution_path = run_with_timeout(
                             name,
                             poss_solution_path, 
                             max_recursion = max_recursion - 1, 
                             timeout = timeout, 
                             verbose = self.verbose)
            
            '''
            child_solution_path = self._get_possible_solution_path(
                                    poss_solution_path, 
                                    max_recursion = max_recursion - 1)

            if not child_solution_path:
                continue
            
            solution_path['valid_answer'] = True
            solution_path['child_solution_path'] = child_solution_path
            return solution_path
            pass
        
        return None
        pass
    
    def _possible_func_sets(self, f, x, x0):
        func_sets = [] 
        terms = []
        term_values = []
        non_terms = [1]
        for term in f.args:
            if not x in term.free_symbols:
                non_terms.append(term)
                continue
            
            limit_value = self._get_limit(term, x, x0)
            
            if limit_value is None:
                return None
            
            terms.append(term)
            term_values.append(limit_value)
            pass

        non_terms = reduce(mul, non_terms)

        if len(terms) < 2:
            return None
        
        for i in range(len(terms)):
            g = terms[i]
            h = [t for ti, t in enumerate(terms)
                 if ti != i]
            h = reduce(mul, h)
            
            g1 = non_terms * g
            h1 = 1/h
            if self._is_valid_set(g1, h1):
                if not self._exists_set(g1, h1, func_sets):
                    func_sets.append([g1, h1])
                
            g1 = non_terms * h
            h1 = 1/g
            if self._is_valid_set(g1, h1):
                if not self._exists_set(g1, h1, func_sets):
                    func_sets.append([g1, h1])
            pass

        func_sets = sorted(func_sets, 
                           key=lambda term: len(f"{term}"), 
                           reverse=False)

        return func_sets
        pass

    def _is_valid_set(self, g, h):
        if not ( isinstance(g, Pow) and isinstance(h, Pow) ):
            return True
        if g.args[1] == -1 and h.args[1] == -1:
            return False
        
        return True
        pass
    
    def _exists_set(self, g1, h1, func_sets):
        temp_sets = self._tried_steps.copy()
        temp_sets.extend(func_sets)
        
        for g, h in temp_sets:
            if g == g1 and h == h1:
                return True
            if g == h1 and h == g1:
                return True
            pass
        return False
        pass

    def _is_lhopital_expr(self, f, x, x0):
        if not isinstance(f, Mul):
            return False
      
        try:
            lhopital_terms = [term for term in f.args
                              if x in term.free_symbols ]
            
            if len(lhopital_terms) < 2:
                return False

            limit_values = []
            for term in lhopital_terms:
                limit_value = self._get_limit(term, x, x0)
                if limit_value is None:
                    return False
                
                abs_limit_value = abs(limit_value)
                limit_values.append(abs_limit_value)
                pass

            if 0 in limit_values and oo in limit_values:
                sOut = f"""
                f = {f}
                x0 = {x0}
                lhopital_terms = {lhopital_terms}
                limit_values = {limit_values}
                """
                self._log(sOut)
                return True
        
        except Exception as e:
            sError = f"""Error 1. {self.__class__.__name__}.{inspect.stack()[0][3]}: {e}
            f = {f}
            x = {x}
            x0 = {x0}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            pass
        
        return False
        pass
    
    def _is_lhopital_pair(self, g_diff_limit, h_diff_limit):
        
        if (g_diff_limit == 0 and h_diff_limit == 0):
            return True
        
        if (abs(g_diff_limit) == oo and abs(h_diff_limit) == oo):
            return True
        
        if (str(g_diff_limit).count(str(oo)) > 0 and
            str(h_diff_limit).count(str(oo)) > 0 ):
            return True
        
        return False
        pass

    def get_applicable_statement(self, g, h, x, x0, label,
                                 has_lHopital_parent = False,
                                 is_lHopital = True):
        f = Function('f')
        f = f(x)
        f = g / h
        
        g_Lim = Limit(self._convert_term_to_symb(g), x, x0)
        h_Lim = Limit(self._convert_term_to_symb(h), x, x0)
        
        g_limit = self._get_limit(g, x, x0)
        h_limit = self._get_limit(h, x, x0)
        
        lmt_stmt = f"\\lim _{{x \\rightarrow {latex(x0)}}}"
        
        f_Lim = Limit(f, x, x0)
        
        f_frac = latex_fraction(latex(g), 
                                latex(h))
        
        
        f_lim_frac = latex_fraction(latex(g_Lim), 
                                    latex(h_Lim))
        
        x0_symb = self._convert_term_to_symb(x0)
        f_lim_subs_frac = latex_fraction(latex(g.subs(x, x0_symb)), 
                                         latex(h.subs(x, x0_symb)))
        
        f_lim_doit_frac = latex_fraction(latex(g_limit), 
                                         latex(h_limit))
        
        appl_statement = []
        
        
        if not has_lHopital_parent:
            statement = f"""
            $\\displaystyle 
            {latex(label)}
            = {latex(f_Lim)}
            $
            """
            appl_statement.append(statement.strip())


        statement = f"""
        $\\displaystyle 
        = {lmt_stmt}\\left( {f_frac} \\right) 
        $
        
        $\\displaystyle 
        = {f_lim_frac}
        $
        
        $\\displaystyle 
        = {f_lim_subs_frac}
        $

        $\\displaystyle 
        = {f_lim_doit_frac}
        $
        """
        appl_statement.append(statement.strip())
        
        if is_lHopital:
            statement = f"<b>Hence, we can apply L’Hopital’s Rule.</b>"
            appl_statement.append(statement.strip())
        
        
        appl_statement = "\n\n".join(appl_statement)
        return appl_statement.strip()
        pass


    def get_random_expr(self, x = None, advanced = False):
        if x is None:
            x = Symbol("x")
            
        if advanced:
            g, h, x0 = random.choice(self.get_exprs_level2(x))
        else:
            g, h, x0 = random.choice(self.get_exprs_level1(x))
        return g, h, x, x0
        pass

    def get_exprs_level1(self, x):
        a = Symbol("a")
        g = Function('g')
        h = Function('h')
        
        g = g(x)
        h = h(x)

        exprs = [
                
            [x, exp(x), oo],

            [exp(-x), 1/x, oo],

            [x**2 - a**2, x - a, a],

            [2 * x + 7, 3 * x**2 - 5, oo],

            [log(x), 1/x, 0],

            [sin(x), x, 0],
            
            [tan(x), x, 0],
            
            [exp(x) - 1, x ** 2 + x, 0],
            
            [(exp(x) - 1) / x, x, 0],
            
            [2 * sin(x) - sin(2 * x), x - sin(x), 0],
                
            [exp(x) - 1, x, 0],
                
            [x ** (1 / (1 - x)), 1, 1],
                
            [x * log(x), 1, 0],
            
            [sin(3 * x), 4 * x, 0],
            ]

        return exprs[:]
        pass

    def get_exprs_level2(self, x):
        a = Symbol("a")
        g = Function('g')
        h = Function('h')
        
        g = g(x)
        h = h(x)

        exprs = [
                [sin(2*x), x**2 + 2*x, 0],
                [exp(2*x), x**2, oo],
                [(log(x))**2, exp(2*x), oo],
                [(log(x))**2, 1/x, 0],
                [log(x), 1/tan(x), 0],
                
                [log(1 + 1/x), 1/x, 0],
                [ x * log(1 + 1/x) - 1, 1/x, 0],
                [ log(log(x)), x, oo],
                [ x, log(log(x)), 0],
                [log(x), sqrt(x), oo],
                [log(tan(x)), 1/x**2, 0],
                [log(1-x), x, 0],
                [log(1 + 3/x), 1/x, 0],
                [log(x), 1/sin(x), 0],

                [exp(x) + 2/x, exp(x) + 5/x, oo],
                
                [x**2 - x, sqrt(x**2 + 1) + sqrt(x + 1), oo],

                [3*x + 2**x, 2*x + 3**x, oo],

                [3*x + 2**x, 2*x + 3**x, oo],
            ]

        return exprs[:]
        pass


    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
    
    
    def _log_DELETED(self, sOut, to_print = None):
        if to_print is None:
            to_print = self.verbose
            
        if to_print:
            print(sOut)
    
    pass
#class LHopitalsHelper:
