'''
Created on 12-Jul-2021

@author: bthakur
'''
import random
import inspect
import traceback

from sympy import (symbols, Symbol, Function, pi, latex,
                   Rational, Derivative, 
                   UnevaluatedExpr as ueval,
                   )
import sympy as sp

from functools import reduce 
import operator

from xv.util import UtilClass

from xv.km import ExtendedPde
from xv.km.util import PintUnitSystem
from xv.util.functions._util_functions import toIntOrFloat


# _applied_differentiation_helper
# AppliedDifferentiationHelper


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

    def get_data_method_names(self):
        data_method_names = [
            '_get_radioactive_decay_1',
            '_get_radioactive_decay',
            ]
        return data_method_names


    def get_random_relation(self, *args, **kwargs):
        data_method = self.get_random_data_method(*args, **kwargs)
        data = data_method()

        lhs_symbol, const_symbols, rhs_symbols = data['prop_symbols'] 
        t_lhs, t_const, t_rhs = data['prop_terms'] 
        label_dict = data['label_dict'] 
        
        rhs = t_const * reduce(operator.mul, t_rhs, 1)        
        eq = ExtendedPde(t_lhs, rhs)
        
        if not eq.is_prop_eq():
            sError = "It is not a proportional equation."
            raise Exception(sError)
        
        eq.set_label_dict(label_dict)
        eq.set_prop_terms(lhs_symbol, rhs_symbols)
        return {'eq': eq}
        pass

    def get_random_computation(self, *args, **kwargs):
        data_method = self.get_random_data_method(*args, **kwargs)
        data = data_method()
        #data = self._get_electrostatic_force()
        
        lhs_symbol, const_symbols, rhs_symbols = data['prop_symbols'] 
        t_lhs, t_const, t_rhs = data['prop_terms'] 
        label_dict = data['label_dict'] 
        var_values = data['var_values'] 
        
        rhs = t_const * reduce(operator.mul, t_rhs, 1)        
        eq = ExtendedPde(t_lhs, rhs)
        
        if not eq.is_prop_eq():
            sError = "It is not a proportional equation."
            raise Exception(sError)
        
        eq.set_label_dict(label_dict)
        eq.set_prop_terms(lhs_symbol, rhs_symbols)
        
        
        return {'eq': eq, 
                'var_values': var_values,
                }
        pass

    def _get_delta(self, symb):
        return Symbol(rf'd{symb}')

    def _get_radioactive_decay_1(self):
        N, t = symbols(r'N, t')
        radio_const = Symbol(r'\lambda')
        
        delta_N = self._get_delta(N)
        delta_t = self._get_delta(t)

        label_dict = {
            t: f"point of time",
            delta_t: "infinitesimal time duration",
            N: f"number of atoms",
            delta_N: f"number of atoms decayed",
            radio_const: "radioactive decay constant",
        }

        ps = PintUnitSystem()
        ps.ureg.define('atom = atom')
        
        N_val = random.randint(1, 100) * 10 ** random.randint(1, 5)
        delta_t_val = random.randint(1, 20) 
        radio_const_val = random.randint(1, 20) 
        delta_N_val = - N_val * radio_const_val * delta_t_val

        var_values = {
            N: N_val * ps.get_quantity(r'atom'),
            radio_const: radio_const_val * ps.get_quantity(r'1/second'), 
            delta_t: delta_t_val * ps.get_quantity(r'second'),
            delta_N: delta_N_val * ps.get_quantity(r'atom'), 
        }

        lhs_symbol = delta_N
        const_symbols = [radio_const]
        rhs_symbols = [N, delta_t]

        t_lhs = -delta_N
        t_const = radio_const
        t_rhs = [N, delta_t]

        data = {
            'prop_symbols': [lhs_symbol, const_symbols, rhs_symbols],
            'prop_terms': [t_lhs, t_const, t_rhs],
            'label_dict': label_dict,
            'var_values': var_values,
            }

        return data

        pass

    def _get_radioactive_decay(self):
        N, t = symbols(r'N, t')
        radio_const = Symbol(r'\lambda')
        
        delta_N = self._get_delta(N)
        delta_t = self._get_delta(t)
        
        diff_N_t = Derivative(N, t)

        label_dict = {
            diff_N_t: "Rate of decay of atoms",
            t: f"point of time",
            delta_t: "infinitesimal time duration",
            N: f"number of atoms",
            delta_N: f"number of atoms decayed",
            radio_const: "radioactive decay constant",
        }

        ps = PintUnitSystem()
        ps.ureg.define('atom = atom')
        
        N_val = random.randint(1, 100) * 10 ** random.randint(1, 5)
        delta_t_val = random.randint(1, 20) 
        radio_const_val = random.randint(1, 20) 
        delta_N_val = - N_val * radio_const_val * delta_t_val

        var_values = {
            N: N_val * ps.get_quantity(r'atom'),
            radio_const: radio_const_val * ps.get_quantity(r'1/second'), 
            delta_t: delta_t_val * ps.get_quantity(r'second'),
            delta_N: delta_N_val * ps.get_quantity(r'atom'), 
        }

        lhs_symbol = delta_N
        const_symbols = [radio_const]
        rhs_symbols = [N]

        t_lhs = - diff_N_t
        t_const = radio_const
        t_rhs = [N]

        data = {
            'prop_symbols': [lhs_symbol, const_symbols, rhs_symbols],
            'prop_terms': [t_lhs, t_const, t_rhs],
            'label_dict': label_dict,
            'var_values': var_values,
            }

        return data

        pass
    
    
    def get_random_approx_value_of_a_function(self, index = None):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        x = Symbol('x')
        deltax = Symbol(f"\\Delta {latex(x)}")
        f = Function('f')(x)
        
        if index is None:
            indexes = list(range(1, 4))
            identifier = f"{self.__class__.__name__}{id(self)}.{inspect.stack()[0][3]}"
            index = self.get_no_repeat_random_choice(identifier, indexes)
        
        if index == 1:
            f_expr = x ** random.randint(2, 3)
            x_val = random.randint(1, 10)
            delta_sign = random.choice([1, -1])
            deltax_val = delta_sign * toIntOrFloat(random.randint(1, 100) / 10 ** random.randint(3, 6))

        if index == 2:
            exponent = random.randint(2, 3)
            f_expr = x ** exponent
            x_val = random.randint(1, 10) * 10 ** random.randint(2, 4)
            delta_sign = random.choice([1, -1])
            deltax_val = delta_sign * random.randint(1, 10)

        if index == 3:
            exponent = Rational(1, random.randint(2, 3))
            f_expr = x ** exponent
            x_val = random.randint(3, 10) ** (1 / exponent)
            delta_sign = random.choice([1, -1])
            deltax_val = delta_sign * random.randint(1, x_val // 2)

        result = {
            'x': x,
            'f': f,
            'f_expr': f_expr,
            'x_val': x_val,
            'deltax': deltax,
            'delta_sign': delta_sign,
            'deltax_val': deltax_val,
            }
        
        return result
        pass

    def get_problem_approx_value_of_a_function(self, **kwargs):
        sInfo = f"""Info: {self.__class__.__name__}.{inspect.stack()[0][3]}
        """
        self._log(sInfo)

        index = kwargs.get('index', None)
        
        func_data = self.get_random_approx_value_of_a_function(index = index)
        
        x = func_data.get('x')
        f = func_data.get('f')
        deltax = func_data.get('deltax')
        f_expr = func_data.get('f_expr')
        x_val = func_data.get('x_val')
        delta_sign = func_data.get('delta_sign')
        deltax_val = func_data.get('deltax_val')
        
        f0_ueval = f_expr.subs(x, ueval(x_val))
        f0_val = f_expr.subs(x, x_val)
        
        f_expr_subs_ueval = f_expr.subs(x, ueval(x_val + deltax_val))
        f_expr_subs = f_expr.subs(x, x_val + deltax_val)
        
        if delta_sign == 1:
            x_plus_deltax_symb = Symbol(f"\\left({latex(x_val)} + {latex(abs(deltax_val))}\\right)")
        else:
            x_plus_deltax_symb = Symbol(f"\\left({latex(x_val)} - {latex(abs(deltax_val))}\\right)")
            
        f_expr_subs_symb = f_expr.subs(x, x_plus_deltax_symb)
        
        
        rate_expr = Derivative(f_expr, x).doit()
        rate_ueval = rate_expr.subs(x, ueval(x_val))
        rate_val = rate_expr.subs(x, x_val)

        f_expr_calc = f0_val + rate_val * deltax_val
        
        prob_s = f"""
        Find the value of $\\displaystyle {latex(f_expr_subs_ueval)}
        $ without using a calculator.
        """
        
        soln_stmts = []
        
        soln_stmt = f"""
        We can rewrite 
        
        $\\displaystyle 
        {latex(f_expr_subs_ueval)}
        = {latex(f_expr_subs_symb)}
        \\qquad 
        \\text{{ where }} {latex(x)} = {latex(x_val)} 
        \\text{{ and }} {latex(deltax)} = {latex(deltax_val)} 
        
        $
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        $\\displaystyle 
        \\begin{{aligned}}
        
        {latex(f.subs(x, x + deltax))} 
        
        & = \\left( \\text{{Value of function}} \\right)
            + 
            \\left( \\text{{Rate of change in function}} \\right)
            \\cdot
            \\left( \\text{{Quantity of change}} \\right)
            \\\\ \\\\
        
        & = {latex(f)} + \\left( {latex(Derivative(f, x))} \\right) 
            \\cdot {latex(deltax)} \\\\ \\\\
        
        & = {latex(f)} + \\left( {latex(Derivative(f_expr, x))} \\right)
            \\cdot {latex(deltax)} \\\\ \\\\
         
        & = {latex(f)} + \\left( {latex(rate_expr)} \\right) 
            \\cdot {latex(deltax)} \\\\ \\\\
        
        & = {latex(f0_ueval)} + \\left( {latex(rate_ueval)} \\right) 
            \\cdot \\left( {latex(deltax_val)} \\right)  \\\\ \\\\
        
        & = {latex(f0_val)} + {latex(rate_val * deltax_val)} \\\\ \\\\
        
        & = {latex(f_expr_calc)} 
        \\qquad \\text{{ actual value is }} {latex(f_expr_subs)}
        \\\\ \\\\
        
        \\end{{aligned}}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        """
        soln_stmts.append(soln_stmt.strip())
        
        answer_s = f"""
        $\\displaystyle {latex(f_expr_calc)}$
        """
        
        
        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        return result
        pass

    pass
#class AppliedDifferentiationHelper:
