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

# _curve_length_helper
# CurveLengthHelper

from xv.util import UtilClass

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

        pass
    
    def get_random_curve_length_data_set(self, index = None):
        x = symbols('x')
        
        data_sets = []
        r = random.randint(1, 5)
        
        expr = sp.sqrt(r ** 2 - x ** 2 )
        x_limits = [-r, r]
        data_sets.append([expr, x, x_limits])
        
        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])
        
        
        if index is None:
            indexes = list(range(len(data_sets)))
            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_curve_length(self, expr, x, x_limits):
        
        L = Symbol('L')
        f = Function('f')
        f = f(x)

        y = Symbol('y')
        dx = Symbol(f"d{latex(x)}")
        dy = Symbol(f"d{latex(y)}")
        df = Symbol(f"d{latex(f)}")
        dL = Symbol(f"d{latex(L)}")
        
        
        problem_items = []
        answer_items = []
        soln_items = []

        x0, x1 = x_limits

        problem_item = f"""
        Find length of the curve $\\displaystyle 
        {latex(f)} = {latex(expr)}$ between $\\displaystyle 
        {latex(x)} = {latex(x0)}$ and $\\displaystyle 
        {latex(x)} = {latex(x1)}$.
        """
        problem_items.append(problem_item.strip())

        df_dx = Derivative(f, x)

        dexpr_dx = Derivative(expr, x)
        dexpr_dx_doit = dexpr_dx.doit()

        curve_expr_symb = sp.sqrt(1 + df_dx ** 2)
        curve_expr_ueval = sp.sqrt(1 + dexpr_dx ** 2)
        curve_expr_eval = sp.sqrt(1 + dexpr_dx_doit ** 2).simplify()
        
        
        curve_length_symb = Integral(curve_expr_symb, (x, x0, x1))
        curve_length_ueval = Integral(curve_expr_ueval, (x, x0, x1))
        curve_length_eval = Integral(curve_expr_eval, (x, x0, x1))
        
        
        curve_length_indefinite = Integral(curve_expr_eval, x)
        method = curve_length_indefinite.doit
        timeout = 10
        curve_length_indefinite_doit = run_with_timeout(
                          f'Thread_curve_intgration_doit', 
                          method, 
                          (), 
                          timeout = timeout, 
                          verbose = self.verbose)
        
        if not curve_length_indefinite_doit:
            sOut = f""" 
            Failed to compute integration in {timeout} seconds 
            curve_length = {curve_expr_eval}
            """
            self._log(sOut)
            return None

        L1_ueval = curve_length_indefinite_doit.subs(x, ueval(x1))
        L0_ueval = curve_length_indefinite_doit.subs(x, ueval(x0))
        
        L1 = curve_length_indefinite_doit.subs(x, x1).simplify()
        L0 = curve_length_indefinite_doit.subs(x, x0).simplify()
        
        
        
        curve_length = Integral(curve_expr_eval, (x, x0, x1)).doit()  #abs(L1 - L0)
        abs_curve_length = abs(curve_length)
        
        dL_ueval = sp.sqrt(dx ** 2 + dy ** 2)
        dL_ueval_subs = sp.sqrt(dx ** 2 + df ** 2)
        

 
        soln_item = f"""
        The length of curve
        
        $\\displaystyle 
        \\begin{{aligned}}
        
        {latex(dL)} 
        & = {latex(dL_ueval)}  
        \\\\ \\\\
        
        & = {latex(dL_ueval_subs)}  
        \\\\ \\\\
        
        & = {latex(curve_expr_symb)}  \\; {latex(dx)}
        
        \\end{{aligned}}
        $
        """
        soln_items.append(soln_item.strip())

        
       
        self._log(f"""
        L0 = {type(L0)} = {L0}

        L1 = {type(L1)} = {L1}
        """)

        if isinstance(L0, type(sp.nan)) or isinstance(L1, type(sp.nan)):
            L0_L1_stmt = ""
        else:
            L0_L1_stmt = f"""
            & = 
            \\left| 
            \\left( {latex(L1)} \\right) - \\left( {latex(L0)} \\right)
            \\right|
            \\\\ \\\\
    
            & = 
            \\left| 
            \\left( {latex(L1 - L0)} \\right)
            \\right|
            \\\\ \\\\
            """
            pass
        
        soln_item = f"""
        
        $\\displaystyle 
        \\begin{{aligned}}
        
        \\implies 
        {latex(L)} 
        & = \\left| {latex(curve_length_symb)} \\right|
        \\\\ \\\\
        
        & = \\left| {latex(curve_length_ueval)} \\right|
        \\\\ \\\\
        
        & = \\left| {latex(curve_length_eval)} \\right|
        \\\\ \\\\
        
        & = \\left| 
        \\left . {latex(curve_length_indefinite_doit)} \\right|^{{ {x1} }}_{{ {x0} }}
        \\qquad
        \\right|
        \\\\ \\\\
        
        & = 
        \\left| 
        \\left( {latex(L1_ueval)} \\right) - \\left( {latex(L0_ueval)} \\right)
        \\right|
        \\\\ \\\\

        {L0_L1_stmt}

        & = {latex(abs_curve_length)} 

        \\end{{aligned}}
        $
        """
        soln_items.append(soln_item.strip())

        
        
        answer_item = f"""
        $\\displaystyle {latex(abs_curve_length)}$
        """
        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(x0), float(x1)])

        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.clf()
        plt.close()
        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 = 'grey')
        ax.plot((x_range[1], x_range[1]), (0, y_range[1]), color = 'grey')
        

        from matplotlib.patches import Polygon

        mini_xs, mini_ys = self.get_small_range(xs, ys)

        verts = [*zip(mini_xs, mini_ys), (mini_xs[-1], mini_ys[0]), (mini_xs[0], mini_ys[0])]
        poly = Polygon(verts, facecolor='0.9', edgecolor='0.5')
        ax.add_patch(poly)
        
        ax.text(mini_xs[0], mini_ys[0] - (mini_ys[-1] - mini_ys[0]), f"$d{latex(x)}$")
        ax.text(mini_xs[-1] + (mini_xs[-1] - mini_xs[0]) , (mini_ys[-1] + mini_ys[0]) / 2, f"$dy$")
                
        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 get_small_range(self, xs, ys):
        
        max_abs_ys = max([abs(y) for y in ys] )
        
        idxs = [idx for idx, y in enumerate(ys[5:-5]) 
                if abs(y) >= max_abs_ys / 4 and abs(y) <= max_abs_ys / 2 ]
        
        if len(idxs) <= 11:
            idx = idx[0]
        else:
            idx = random.choice(idxs[3:-3])
            pass
        
        
        
        mini_xs = []
        mini_ys = []
        idx_range = min(len(xs) // 10, 5)
        
        for idx in range(idx, idx + idx_range):
            mini_xs.append(xs[idx])
            mini_ys.append(ys[idx])
            pass
        
        return (mini_xs, mini_ys)
        pass

    pass
#class CurveLengthHelper:
