'''
Created on 12-Jul-2021

@author: bthakur

'''
import inspect
import traceback

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

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

import sympy as sp
import numpy as np


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,
                   pi,
                   )

import matplotlib.pyplot as plt
from xv.plotter import Plotter2D
from xv.km.ui import SvgHelper

from ._derivative_solver import DerivativeSolver

from xv.util import run_with_timeout

# area_under_curve_helper
# AreaUnderCurveHelper

from xv.util import UtilClass
from xv.util.functions._util_functions import list_to_str

class AreaUnderCurveHelper(UtilClass):
    
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        pass
    
    def get_random_area_curve_data_set(self, index = None):
        x = symbols('x')
        
        data_sets = []
        
        expr = - x**2 + 4 * x 
        x_limits = [1, 2]
        data_sets.append([expr, x, x_limits])
        
        expr = - x**2 - 6 * x + 5
        x_limits = [1, 2]
        data_sets.append([expr, x, x_limits])
        
        expr = - x**2 + 4 * x + 1
        x_limits = [1, 2]
        data_sets.append([expr, x, x_limits])
        
        expr = - 4 * x**2 + 4 * x + 3
        x_limits = [1, 2]
        data_sets.append([expr, x, x_limits])
        
        expr = - x**3 + 7 * x**2 - 10 * x + 5
        x_limits = [1, 2]
        data_sets.append([expr, x, x_limits])
        
        expr = sqrt((1+x)/(1-x))
        x_limits = [-1, 1]
        data_sets.append([expr, x, x_limits])
        
        expr = sin(x)
        x_limits = [-pi, pi]
        data_sets.append([expr, x, x_limits])
        
        indexes = list(range(len(data_sets)))
        
        if index is None:
            identifier = f"{self.__class__.__name__}{id(self)}.{inspect.stack()[0][3]}.data_sets"
            index = self.get_no_repeat_random_choice(identifier, indexes)
        
        return data_sets[index]
        pass

    def get_area_under_curves(self, exprs, x, x_limits):
        
        if len(exprs) == 1:
            return self.get_area_under_a_curve(exprs[0], x, x_limits)
        
        pass

    def get_area_under_a_curve(self, expr, x, x_limits):
        
        
        A = Symbol('A')
        f = Function('f')
        f = f(x)
        
        problem_items = []
        answer_items = []
        soln_items = []


        problem_item = f"""
        Find the area under curve $\\displaystyle 
        {latex(f)} = {latex(expr)}$ between $\\displaystyle 
        {latex(x)} = {latex(x_limits[0])}$ and $\\displaystyle 
        {latex(x)} = {latex(x_limits[-1])}$
        """
        problem_items.append(problem_item.strip())

        area_expr = expr
        area = Integral(f, (x, x_limits[0], x_limits[-1]))

        soln_item = f"""
        The area $\\displaystyle {latex(A)}$ is defined as:
        $\\displaystyle 
        {latex(A)} = {latex(area)}$
        """
        soln_items.append(soln_item.strip())

        transformed_result = self._transform_x_limits(expr, x, x_limits)
        print(f"""
        transformed_result
        {transformed_result}
        """)
        
        if not transformed_result is None:
            x_limits = transformed_result['mod_x_limits']
            roots_soln_s = transformed_result['soln_s']
            soln_items.append(roots_soln_s.strip())
            pass

        #convert each limit pair 
        x_limit_pairs = [(x_limits[idx], x_limits[idx + 1]) for idx in range(len(x_limits) - 1)]
        
        area_segments = [Integral(f, (x, x0, x1))  
                         for x0, x1 in x_limit_pairs]
        
        area_expanded_segments = [f"\\left| {latex(area_segment)} \\right|" 
                                  for area_segment in area_segments]
        
        area_expanded = " + ".join(area_expanded_segments)


        soln_item = f"""
        As area may be negative or positive, we have to break into segments of negative and positive areas, take absolute and add them to get the area.
        
        $\\displaystyle 
        \\implies
        {latex(A)} = {area_expanded}$
        """
        soln_items.append(soln_item.strip())


        I = Symbol('I')
        I_f = Integral(f, (x))
        I_expr = Integral(area_expr, (x))
        
        method = I_expr.doit
        timeout = 5
        I_expr_doit = run_with_timeout(
                          f'Thread_intgration_doit', 
                          method, 
                          (), 
                          timeout = timeout, 
                          verbose = self.verbose)
        
        
        if not I_expr_doit:
            sOut = f""" 
            Failed to compute integration in {timeout} seconds 
            I_expr = {I_expr}
            """
            self._log(sOut)
            return None
        
        soln_item = f"""
        Now, 
        $\\displaystyle 
        {latex(I)} 
        = {latex(I_f)} 
        = {latex(I_expr)} 
        = {latex(I_expr_doit)}$
        """
        soln_items.append(soln_item.strip())


        area_expanded_segments = [f"\\left| {latex(I_expr_doit)} \\right|^{{{x1}}}_{{{x0}}}" 
                                  for x0, x1 in x_limit_pairs]
        
        area_expanded = " + ".join(area_expanded_segments)

        soln_item = f"""
        $\\displaystyle 
        \\implies
        {latex(A)} = {area_expanded}$
        """
        soln_items.append(soln_item.strip())


        area_expanded_segments = [f"""
        \\left| 
        
        \\left( {latex(I_expr_doit.subs(x, ueval(x1)))} \\right)   
        - 
        \\left( {latex(I_expr_doit.subs(x, ueval(x0)))} \\right)   
        
        \\right|
        """ 
        for x0, x1 in x_limit_pairs]
        
        area_expanded = " + ".join(area_expanded_segments)

        soln_item = f"""
        Substituting limits:
        
        $\\displaystyle 
        \\implies
        {latex(A)} = {area_expanded}$
        """
        soln_items.append(soln_item.strip())


        
        area_expanded_segments = [f"""
        \\left| 
        
        \\left( {latex(I_expr_doit.subs(x, x1))} \\right)   
        - 
        \\left( {latex(I_expr_doit.subs(x, x0))} \\right)   
        
        \\right|
        """ 
        for x0, x1 in x_limit_pairs]
        
        area_expanded = " + ".join(area_expanded_segments)

        soln_item = f"""
        $\\displaystyle 
        \\implies
        {latex(A)} = {area_expanded}$
        """
        soln_items.append(soln_item.strip())


        
        area_segments = [Integral(expr, (x, x0, x1)).doit()  
                         for x0, x1 in x_limit_pairs]
        
        area_expanded_segments = [f"\\left| {latex(area_segment)} \\right|" 
                                  for area_segment in area_segments]
        
        area_expanded = " + ".join(area_expanded_segments)
        
        area_segments_abs = [abs(area_segment)  for area_segment in area_segments]
        
        area = Add(*area_segments_abs)

        soln_item = f"""
        $\\displaystyle 
        \\implies
        {latex(A)} = {area_expanded} = {latex(area)}$
        """
        soln_items.append(soln_item.strip())

        
        answer_item = f"""
        $\\displaystyle {latex(area)}$
        """
        answer_items.append(answer_item.strip())


        problem_s = "\n\n".join(problem_items)
        soln_s = "\n\n".join(soln_items)
        answer_s = "\n\n".join(answer_items)


        svg_str = self.plot_expr(expr, x, x_range = [float(x_limits[0]), float(x_limits[-1])])

        result = {"problem_s": problem_s,
                  "soln_s": soln_s,
                  "svg_str": svg_str,
                  "answer_s": answer_s,
                  }
        return result
        pass
            
    def plot_expr(self, expr, x, x_range = None):
        verbose = self.verbose
        plt.ioff()
 
        from sympy import symbols, Matrix
        import numpy as np
        
        plotter = Plotter2D(verbose = verbose)
        
        x_range.sort()
        fig, ax, xs, ys = plotter.plot_expr(expr, x_range = x_range, return_xy_data = True )
        
        y_range = self._get_y_range(expr, x, x_range) 
        
        #plot horizontal lines
        ax.plot(x_range, (0, 0), color = 'red')
        
        #plot vertical lines
        ax.plot((x_range[0], x_range[0]), (0, y_range[0]), color = 'green')
        ax.plot((x_range[1], x_range[1]), (0, y_range[1]), color = 'green')
        
        from matplotlib.patches import Polygon
        verts = [*zip(xs, ys), (xs[-1], 0), (xs[0], 0), (xs[0], ys[0])]
        poly = Polygon(verts, facecolor='0.9', edgecolor='0.5')
        ax.add_patch(poly)
        
                
        svgHelper = SvgHelper()
        svg_str = svgHelper.plt_to_svg()
        
        plt.clf()
        plt.close()
        
        return svg_str  
    
    
    def _get_y_range(self, expr, x, x_range):  
        try:
            y_range = []
            y_range.append(expr.subs(x, x_range[0]))
            y_range.append(expr.subs(x, x_range[-1]))
            return y_range
        except:
            pass
        
        x_range = np.linspace(*x_range, 1000)
        x_range = [x_range[1], x_range[-1]]

        try:
            y_range = []
            y_range.append(expr.subs(x, x_range[0]))
            y_range.append(expr.subs(x, x_range[-1]))
            return y_range
        except:
            pass
        
        return None
        pass  
    
    
    def _transform_x_limits(self, expr, x, x_limits):
        

        try:        
            if len(x_limits) != 2:
                return None

            x_limits = list(x_limits)
            x_limits.sort()
        
            eq = Eq(expr, 0)
            
            method = sp.solve
            timeout = 5
            x_vals = run_with_timeout(
                              f'Thread_{self.__class__.__name__}.{inspect.stack()[0][3]}_solve_eq', 
                              method, 
                              (eq, x), 
                              timeout = timeout, 
                              verbose = self.verbose)
            
            
            if not x_vals:
                sOut = f""" 
                Failed to solve equation in {timeout} seconds 
                eq = {eq}
                returning {x_limits}.
                """
                self._log(sOut)
                return None
            
            #remove non-real values
            x_vals = [x_val  for x_val in x_vals if sp.sympify(x_val).is_real ]
            
            x_vals = [x_val  for x_val in x_vals 
                      if x_val > x_limits[0] and x_val < x_limits[-1] ]
            
            x_vals.sort()
            
            mod_x_limits = [x_limits[0], *x_vals, x_limits[-1]]
            mod_x_limits = list(set(mod_x_limits))
            mod_x_limits.sort()
            if len(mod_x_limits) == 2:
                return None
            
            x_vals_str = list_to_str(x_vals, last_sep = 'and', to_latex = True)
            
            x_limit_pairs = [(mod_x_limits[idx], mod_x_limits[idx + 1]) 
                             for idx in range(len(mod_x_limits) - 1)]
            
            x_limit_pairs_str = [f"$\\displaystyle {latex(pair)} $" for pair in x_limit_pairs]
            
            x_limit_pairs_str = list_to_str(x_limit_pairs_str, last_sep = 'and')
            
            soln_s = f"""
            The curve $\\displaystyle {latex(expr)}
            $ crosses ${latex(x)}$-axis at $\\displaystyle {latex(x)} = {x_vals_str}
            $. Therefore, we can split the range into {x_limit_pairs_str}.
            """
            
            result = {
                'mod_x_limits': mod_x_limits,
                'roots': x_vals,
                'eq': eq,
                'soln_s': soln_s.strip(),
                }
            
            return result
        
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}: {e}
            x_limits = {x_limits}
            returning {x_limits}.
            """
            self._log(sError)
            self._log(traceback.format_exc())
            return None
            pass
        pass
         
    pass
#class AreaUnderCurveHelper:
