'''
Created on 20-Jan-2022

@author: bthakur
'''

import codecs
import inspect
import traceback
import random
import numpy as np
from sympy import (latex, Symbol, symbols,
                    Rational,
                    sin, cos, tan,
                    asin, acos, atan,
                    sinh, cosh, tanh,
                    log, exp, sqrt,
                   diff, factor, 
                   Function, Eq,
                   Add, Mul, Pow,
                   simplify, solve, factorial,
                   Sum, binomial,
                   oo
                   )

from sympy.core.numbers import One, Integer
from sympy.series import series

from xv.util import toIntOrFloat

from xv.km.util import latex_fraction, latex_color
from sympy.core.expr import UnevaluatedExpr as ueval

from xv.km.ui import SvgHelper
from xv.util.data import CacheUtil

import matplotlib.pyplot as plt
from xv.km.kelements import KProblemTemplate


# binomial_helper

class BinomialHelper:
    '''
    This class deals with expressions of two variable and their applications.
    For more than two variables, see MultinomialHelper
    '''
    _DEFAULT_TERM_COUNT = 4
    _MAX_DISPLAYABLE_TERM_COUNT = 11
    
    def __init__(self, **kwargs):
        self.verbose = kwargs.get('verbose', False)
        self.x = Symbol('x')
        self.y = Symbol('y')
        self.n = Symbol('n', integer=True, nonnegative=True)
        self.k = Symbol('k', integer=True, nonnegative=True)
        pass

    def get_expr(self, x = None, y = None, n = None):
        x, y, n = self._resolve_xyn(x, y, n)
            
        expr_symbol = Symbol(f"""\\left({latex(x)} + {latex(y)}\\right)""")

        expr = ueval(expr_symbol) ** ueval(n)
        
        return expr    
        pass



    def get_series_sum(self, x = None, y = None, n = None, k = None):

        x, y, n = self._resolve_xyn(x, y, n)
        
        if k is None:
            k = self.k
            
        if self._exists_int_binomial(n):
            n_lower = 0
            n_upper = n
        else:
            n_lower = 0
            n_upper = oo
            
        coeff_term = Symbol(f"\\binom{{{latex(n)}}}{{{latex(k)}}}")
        x_term = x ** ueval(n - k)
        y_term = y ** ueval(k)
        term = ueval(coeff_term) * ueval(x_term) * ueval(y_term)
        
        series_sum = Sum(term, (k, n_lower, n_upper))

        return series_sum
        pass
    
    def _get_factorial_factors(self, n, i, evaluate = False):
        if i != abs(int(i)):
            return None
        if i == 0:
            return 1
        factorial_factors = []
        for idx in range(i):
            i_term = n - idx
            if not evaluate:
                i_term = ueval(i_term)
                
            factorial_factors.append(i_term)
            pass
        
        factorial_factors = Mul(*factorial_factors)
        return factorial_factors
        
    
    def _exists_int_binomial(self, n):
        try:
            if hasattr(n, 'is_integer'):
                if n.is_integer and n >= 0:
                    return True
                 
            if isinstance(n, int) and n > 0:
                return True

            if abs(int(n)) == n:
                return True
        except:
            return False
        pass


    def get_series_latex(self, 
                   x = None, y = None, n = None, 
                   term_count = None,
                   evaluate_level = 0
                   ):
        '''
        if evaluate_level is None, it will return all levels
        
        evaluate_level:
        0 = no evaluation
        1 = part evalulation of terms 
        2 = full evaluation of terms
        3 = full evaluation of series
        '''
        if not evaluate_level is None:
            series = self._get_series_latex( 
                       x = x, y = y, n = n, 
                       term_count = term_count,
                       evaluate_level = evaluate_level
                       )
            return series
        
        series_list = []
        for evaluate_level in range(0, 4):
            series = self._get_series_latex( 
                       x = x, y = y, n = n, 
                       term_count = term_count,
                       evaluate_level = evaluate_level
                       )
            series_list.append(series)
            pass
        
        return series_list
        pass

    def _get_series_latex(self, 
                   x = None, y = None, n = None, 
                   term_count = None,
                   evaluate_level = 0
                   ):
        '''
        evaluate_level:
        0 = no evaluation
        1 = part evalulation of terms 
        2 = full evaluation of terms
        3 = full evaluation of series
        '''
        series_terms = self.get_series_terms(x = x, y = y, n = n, 
                                             term_count = term_count, 
                                             evaluate_level = evaluate_level)
        
        
        cdots = []
        if (not self._exists_int_binomial(n) or  
            not self._is_series_length_finite(series_terms, n)):
            cdots.append(f"\\cdots")
            
        
        #simplify the series 
        if evaluate_level == 3:
            series_terms = [Add(*series_terms)]
        
        
        series_terms = [f"{latex(term)}" for term in series_terms]
        
        series_terms.extend(cdots)    
        
        series = " + ".join(series_terms)
        
        return series

    def _is_series_length_finite(self, series_terms, n):
        try:
            if len(series_terms) < n + 1:
                return True
        except:
            pass
        return False
        pass
    

    def get_series_terms(self, 
                   x = None, y = None, n = None, 
                   term_count = None,
                   evaluate_level = 0
                   ):
        '''
        evaluate_level:
        0 = no evaluation
        1 = part evalulation of terms 
        2, 3 = full evaluation of terms
        '''

        x, y, n = self._resolve_xyn(x, y, n)

        term_count = self._get_displayable_term_count(n, term_count)
        
        series_terms = []
        for i in range(term_count):
            term = self.get_ith_term(i, x, y, n, evaluate_level = evaluate_level)
            series_terms.append(term)
            pass

        return series_terms
        pass


    def get_ith_term(self, i, x = None, y = None, n = None, evaluate_level = 0):
        '''
        evaluate_level:
        0 = no evaluation
        1 = part evalulation of terms 
        2, 3 = full evaluation of terms
        '''
            
        if self._exists_int_binomial(n):
            term_coeff =  Symbol(f"\\binom{{{n}}}{{{i}}}")
            eval_term_coeff_1 = binomial(n, n-i)
            eval_term_coeff_2 = eval_term_coeff_1
        else:
            coeff_num = self._get_factorial_factors(n, i)
            coeff_num_eval = self._get_factorial_factors(n, i, evaluate = True)
            coeff_den = factorial(ueval(i))
            coeff_den_eval = factorial(i)
            
            term_coeff = Symbol(latex_fraction(latex(coeff_num), latex(coeff_den)))
            eval_term_coeff_1 = Symbol(f"{latex(coeff_num_eval / coeff_den_eval)}")
            eval_term_coeff_2 = coeff_num_eval / coeff_den_eval
            pass


        if (evaluate_level == 2 or evaluate_level == 3):
            eval_term_x = toIntOrFloat(x ** (n-i)) 
            eval_term_y = toIntOrFloat(y ** i)
            return eval_term_coeff_2 * eval_term_x * eval_term_y
            
        if evaluate_level == 1:
            eval_term_x = toIntOrFloat(x ** (n-i))
            eval_term_y = toIntOrFloat(y ** i)
            eval_term = self._get_prod_latex_with_bracket(
                                [eval_term_coeff_1, eval_term_x, eval_term_y])
            
            return eval_term
            pass
        
            
        term_x = x ** ueval(n-i)
        term_y = y ** ueval(i)
        ueval_term = self._get_prod_latex_with_bracket([term_coeff, term_x, term_y])
        return ueval_term
    
    
    def _get_prod_latex_with_bracket(self, arr):
        mod_arr = []
        for item in arr:
            if ("+" in str(item) or 
                "-" in str(item)):
                if not isinstance(item, Pow):
                    item_term = f"\\left ( {latex(item)} \\right)"
                    mod_arr.append(item_term)
                    continue
                pass

            item_term = latex(item)
            mod_arr.append(item_term)
            pass
        
        mod_arr_ltx = f" \\cdot ".join(mod_arr)
        return Symbol(mod_arr_ltx)
        pass
    
    def _resolve_xyn(self, x, y, n):
        if x is None:
            x = self.x
        
        if y is None:
            y = self.y
        
        if n is None:
            n = self.n

        return x, y, n        
        pass
    

    def _get_displayable_term_count(self, n, term_count):
        _MAX_TERM_COUNT = BinomialHelper._MAX_DISPLAYABLE_TERM_COUNT
        _DEFAULT_TERM_COUNT = BinomialHelper._DEFAULT_TERM_COUNT

        if term_count is None:
            if isinstance(n, int) and n > 0:
                term_count = n + 1
            else:
                term_count = _DEFAULT_TERM_COUNT
        
        if term_count > _MAX_TERM_COUNT:
            return _MAX_TERM_COUNT

        return term_count
    
    def get_combination_expr(self, n, r):
        expr_n = factorial(n)
        expr_d = factorial(n-r) * factorial(r)
        return expr_n / expr_d
        pass

    def get_permutation_factorial_latex(self, n, r, evaluate = False, evaluate_n_r = True):
        if not evaluate:
            n_factorial = factorial(ueval(n))

            if evaluate_n_r:
                n_r_factorial = factorial(ueval(n - r))
                n_r_factorial_latex = f"{latex(n_r_factorial)}"
            else:
                n_r_factorial_latex = f"\\left({latex(n)} - {latex(r)}\\right)!"
            
            factorial_latex = latex_fraction(latex(n_factorial), 
                                             f"{n_r_factorial_latex}")  
        else:
            factorial_latex = latex_fraction(latex(factorial(n)), 
                                             f"{latex(factorial(n-r))}")  
                                   
        return factorial_latex
        pass


    def get_combination_factorial_latex(self, n, r, evaluate = False, evaluate_n_r = True):
        if not evaluate:
            n_factorial = factorial(ueval(n))
            r_factorial = factorial(ueval(r))
            if evaluate_n_r:
                n_r_factorial = factorial(ueval(n - r))
                n_r_factorial_latex = f"{latex(n_r_factorial)}"
            else:
                n_r_factorial_latex = f"\\left({latex(n)} - {latex(r)}\\right)!"
            
            factorial_latex = latex_fraction(latex(n_factorial), 
                                             f"{n_r_factorial_latex} \\; {latex(r_factorial)}")  
        else:
            factorial_latex = latex_fraction(latex(factorial(n)), 
                                             f"{latex(factorial(n-r))} * {latex(factorial(r))}")  
                                   
        return factorial_latex
        pass

    def get_binomial_latex(self, n, r):
        return f"\\binom{{{latex(n)}}}{{{latex(r)}}}"
        pass
    
    def get_simple_arrangement_statements(self, n = None, r = None):
        if n is None:
            n = random.randint(5, 10)
        
        if r is None:
            r = random.randint(3, 5)
        
        prob_s = f"""
        If have {n} distinct paintings and {r} places to display them. 
        How many ways are possible to display them?
        """

        prod_form = list(range(n, n - r , -1))
        prod_form = " * ".join([f"{x}" for x in prod_form])

        numr_expr = list(range(n, 0 , -1))
        numr_expr = " * ".join([f"{x}" for x in numr_expr])

        denr_expr = list(range(n - r, 0 , -1))
        denr_expr = " * ".join([f"{x}" for x in denr_expr])

        denr_frac = latex_fraction(denr_expr, denr_expr)
        expd_frac = latex_fraction(numr_expr, denr_expr)
        
        factorial_frac = latex_fraction(latex(factorial(ueval(n))),
                                        latex(factorial(ueval(n - r)))
                                        )

        
        nPr_factorial_latex_gross = self.get_permutation_factorial_latex(
                                        n, r, evaluate_n_r = False)
        nPr_factorial_latex = self.get_permutation_factorial_latex(n, r)
        
        nPr_latex = self.get_permutation_latex(n, r)
        
        soln_s = [] 
        
        soln_item = f"""
        We want to fill {r} places by picking paintings from {n} possible distinct paintings.
        """
        soln_s.append(soln_item.strip())
        
        from xv.util import number_to_nth
        
        for i in range(r):
            if i == 0:
                soln_item = f"""
                For {number_to_nth(i + 1)} place, we can choose any of {n - i} distinct paintings.
                """
            else:
                soln_item = f"""
                Now, we are left with {n} - {i} = {n - i} paintings.
                For {number_to_nth(i + 1)} place, we can choose any of {n - i} distinct paintings.
                """
                
            soln_s.append(soln_item.strip())
            pass


        soln_item = f"""
        Combining all choices, we get all possible ways of displaying paintings
        
        = $\\displaystyle {prod_form}$
        
        It can be written as 
        
        = $\\displaystyle {prod_form} * {denr_frac}$
        
        = $\\displaystyle {expd_frac}$
        
        = $\\displaystyle {nPr_factorial_latex}$
        
        = $\\displaystyle {nPr_factorial_latex_gross}$
        
        It is written as 
        = $\\displaystyle {nPr_latex}$
        
        and read as:
        \t {n} permutation {r}, or
        \t arrange {r} of {n}, or
        \t arrange {r} items from {n} distinct items
        """
        soln_s.append(soln_item.strip())
        
        
        soln_s = "\n\n".join(soln_s)
        
        answer_s = f"""
        $\\displaystyle {prod_form}$, or
        
        $\\displaystyle {nPr_factorial_latex_gross}$, or
        
        $\\displaystyle {nPr_factorial_latex}$, or
        
        $\\displaystyle {nPr_latex}$
        """
        
        result = {
            'prob_s': prob_s,
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        return result
    

    def get_permutation_statements(self, n, r):
        if n >= 0 and r >= 0 and n >= r:
            return self._get_permutation_statements(n, r)
        return self._get_invalid_permutation_statements(n, r)

    def _get_invalid_permutation_statements(self, n, r):
        nPr_latex = self.get_permutation_latex(n, r)
        nPr = 0
        
        conditions = []
        if n < 0:
            conditions.append(f"total count {n} is less than zero.")
            pass
        if r < 0:
            conditions.append(f"desired items in arrangement {r} is less than zero.")
            pass
        if n < r:
            conditions.append(f"total count {n} is less than desired items in arrangement {r}.")
            pass
        
        if conditions:
            conditions = ", \n".join(conditions)
        
        soln_s = f"""
        As:
        {conditions},
        the desired arrangement is not possible.
        
        Therefore, ways of arranging {r} from {n} items
        
        
        
        $\\displaystyle = {nPr_latex}$
        
        $\\displaystyle = {latex(nPr)}$
        
        """
        result = {
            'nPr_latex': nPr_latex,
            'nPr': nPr,
            'soln_s': soln_s
            }
        return result

    def _get_permutation_statements(self, n, r):
        nPr_latex = self.get_permutation_latex(n, r)
        
        nPr_factorial_latex_gross = self.get_permutation_factorial_latex(
                                        n, r, evaluate_n_r = False)
        nPr_factorial_latex = self.get_permutation_factorial_latex(n, r)
        nPr_factorial_evaluated_latex = self.get_permutation_factorial_latex(n, r,
                                                                           evaluate = True)
        nPr = self.get_permutation_expr(n, r)
        
        
        soln_s = f"""
        ways of arranging {r} from {n} items
        
        $\\displaystyle = {nPr_latex}$
        
        $\\displaystyle = {nPr_factorial_latex_gross}$
        
        $\\displaystyle = {nPr_factorial_latex}$
        
        $\\displaystyle = {nPr_factorial_evaluated_latex}$
        
        $\\displaystyle = {latex(nPr)}$
        
        """
        result = {
            'nPr_latex': nPr_latex,
            'nPr_factorial_latex': nPr_factorial_latex,
            'nPr_factorial_evaluated_latex': nPr_factorial_evaluated_latex,
            'nPr': nPr,
            'soln_s': soln_s
            }
        return result

    

    def get_combination_statements(self, n, r):
        if n >= 0 and r >= 0 and n >= r:
            return self._get_combination_statements(n, r)
        
        return self._get_invalid_combination_statements(n, r)

    def _get_invalid_combination_statements(self, n, r):
        
        nCr_latex = self.get_binomial_latex(n, r)
        nCr = 0
        
        conditions = []
        if n < 0:
            conditions.append(f"total count {n} is less than zero.")
            pass
        if r < 0:
            conditions.append(f"desired selection {r} is less than zero.")
            pass
        if n < r:
            conditions.append(f"total count {n} is less than desired selection {r}.")
            pass
        
        if conditions:
            conditions = ", \n".join(conditions)
        
        soln_s = f"""
        As:
        {conditions},
        the selection is not possible.
        
        Therefore, ways of selecting {r} from {n} items
        
        $\\displaystyle = {nCr_latex}$
        
        $\\displaystyle = {latex(nCr)}$
        
        """
        result = {
            'nCr_latex': nCr_latex,
            'nCr': nCr,
            'soln_s': soln_s
            }
        return result

    def _get_combination_statements(self, n, r):
        nCr_latex = self.get_binomial_latex(n, r)
        
        nCr_factorial_latex_gross = self.get_combination_factorial_latex(
                    n, r, evaluate_n_r = False)

        nCr_factorial_latex = self.get_combination_factorial_latex(n, r)
        
        nCr_factorial_evaluated_latex = self.get_combination_factorial_latex(
                    n, r, evaluate = True)
        
        nCr = self.get_combination_expr(n, r)
        
        
        soln_s = f"""
        ways of selecting {r} from {n} items
        
        $\\displaystyle = {nCr_latex}$
        
        $\\displaystyle = {nCr_factorial_latex_gross}$
        
        $\\displaystyle = {nCr_factorial_latex}$
        
        $\\displaystyle = {nCr_factorial_evaluated_latex}$
        
        $\\displaystyle = {latex(nCr)}$
        
        """
        result = {
            'nCr_latex': nCr_latex,
            'nCr_factorial_latex': nCr_factorial_latex,
            'nCr_factorial_evaluated_latex': nCr_factorial_evaluated_latex,
            'nCr': nCr,
            'soln_s': soln_s
            }
        return result

    
    def get_permutation_expr(self, n, r, symb = False):
        if symb:
            if n != r:
                return Symbol(f"""\\frac{{ {latex( factorial(ueval(n)) )} }} 
                                        {{ {latex( factorial(ueval(n-r)) )} }} """)
            else:
                return Symbol(f"""{latex( factorial(ueval(n)) )}""")
                
        expr_n = factorial(n)
        expr_d = factorial(n-r)
        return expr_n / expr_d
        pass
    
    def get_permutation_latex(self, n, r):
        return f"P_{{{latex(r)}}}^{{{latex(n)}}}"
        pass
    
   
    def get_combination_latex(self, n, r):
        return f"C_{{{latex(r)}}}^{{{latex(n)}}}"
        pass


    
    def get_combination_formula_latex(self, n, r):
        stmts = []
        
        nCr = self.get_combination_latex(n, r)
        nPr = self.get_permutation_latex(n, r)

        rPr = self.get_permutation_latex(r, r)
        
        stmt = f"""
        Arranging ${latex(r)}$ out of ${latex(n)}$ things means:
        1. select ${latex(r)}$ out of ${latex(n)}$ things, and
        2. arrange ${latex(r)}$ out of ${latex(r)}$ things.
        """
        stmts.append(stmt.strip())
        
        
        stmt = f"$\\displaystyle \\implies {nPr} = {nCr} * {rPr}$"
        stmts.append(stmt.strip())

        stmt = f"""
        Divide both sides with $\\displaystyle {rPr}$ and swap sides:
        """
        stmts.append(stmt.strip())
        
        
        nCr_frac = latex_fraction(nPr, rPr)
        stmt = f"$\\displaystyle \\implies {nCr} = {nCr_frac}$"
        stmts.append(stmt.strip())
        
        nCr_expr = self.get_combination_expr(n, r)
        
        stmt = f"$\\displaystyle \\implies {nCr} = {latex(nCr_expr)}$"
        stmts.append(stmt.strip())
        
        return stmts
        pass
    
    def get_factorial_latex(self, n, r = 1):
        from xv.util import list_to_str
        factorial_latex = []
        if isinstance(n, int):
            if r > n:
                return "0"
            if n <= 5:
                factorial_latex = reversed(list(range(r, n + 1)))
                factorial_latex = list_to_str(factorial_latex, 
                                              sep = " * ", 
                                              empty_list_as = "0")
                return factorial_latex
            
            factorial_latex.extend([n, n-1])
        else:
            factorial_latex.extend([f"({latex(n)})", f"({latex(n-1)})"])
            pass
        
        factorial_latex.append(f"\\cdots")
        
        
        if r == 1:
            factorial_latex.extend([3, 2, 1])
        else:
            factorial_latex.extend([f"({latex(n)}-{latex(r)}+{2})", f"({latex(n)}-{latex(r)}+{1})"])

            
        factorial_latex = list_to_str(factorial_latex, 
                                      sep = " * ", 
                                      empty_list_as = "0")

        return factorial_latex
        pass
    
    def _get_simple_table(self, n, labels = None):
        
        div_style = """ 
        height: 20px; min-width: 20px; float: left; margin-right: 10px;
        padding: 0 3px 0 3px;
        text-align: center;
        """.replace("\n", " ").strip()
        border_style = """ border: 2px solid red; """
        
        if labels is None:
            labels = ["&nbsp;" for i in range(n)]
        
        tstr = []
        tstr.append("""<ul style = "list-style:none;">""")

        tstr.append(f"""<li style="display: inline;" >""")

        for i in range(n):
            if labels[i] != "...":
                tstr.append(f"""<div style="{div_style} {border_style}" >{labels[i]}</div>""")
            else:
                tstr.append(f"""<div style="{div_style}" >{labels[i]}</div>""")
                
        
        tstr.append("</li>")
        
        tstr.append("</ul>")
        tstr = "".join(tstr)
        return tstr
        pass
    
    def get_permutation_formula_latex(self, n, r):
        stmts = []

        nPr = self.get_permutation_latex(n, r)
        nPr_expanded = f"{n} ({n-1}) ({n-2}) \\; \\cdots \\; ({n}-{r}+{1})"

        rPr_expanded = latex_color(f"({n}-{r}) ({n}-{r}-{1}) \\; \\cdots \\cdot {1}", 'red')
        nPn_expanded = f"{nPr_expanded} \\; \\cdot \\; {rPr_expanded}"
        
        p_fraction = latex_fraction(nPn_expanded, rPr_expanded)

        nPr_expr_symb = self.get_permutation_expr(n, r, symb = True)
        nPr_expr = self.get_permutation_expr(n, r)
        
        tstr_first_place = self._get_simple_table(5, labels = [f'{n}', '', '', '...', ''])
        tstr_second_place = self._get_simple_table(5, labels = [f'{n}', f'{n-1}', '', '...', ''])
        tstr_third_place = self._get_simple_table(5, labels = [f'{n}', f'{n-1}', f'{n-2}', '...', ''])
        tstr_rth_place = self._get_simple_table(5, labels = [f'{n}', f'{n-1}', f'{n-2}', '...', 
                                                             f'{n} - {r} + 1', 
                                                             ])

        stmt = f"""
        Arranging ${latex(r)}$ out of ${latex(n)}$ things.
        
        As we start with ${latex(n)}$ things and r places:
        1. For first place, we can choose any item from ${latex(n)}$ things, so we have ${latex(n)}$ choices.
           {tstr_first_place}

        2. For second place, we can choose any item from remainder ${latex(n -1)}$ things, so we have ${latex(n-1)}$ choices.
           {tstr_second_place}
        
        3. For third place, we can choose any item from remainder ${latex(n -2)}$ things, so we have ${latex(n-2)}$ choices.
        {tstr_third_place}
        ...
        
        Thus, for ${latex(r)}$th place, the choice will be ${latex(n)} - ({latex(r)} - 1) =  {latex(n)} - {latex(r)} + 1$
        {tstr_rth_place}
        
        Now, all choices are dependent on each other, so will get a product to get the result.
        """
        stmts.append(stmt.strip())
        
        try:
            if n > r:
                is_n_gt_r = True
            else:
                is_n_gt_r = False
        except:
            is_n_gt_r = True
        
        if is_n_gt_r:
            stmt = f"""
            $\\displaystyle 
            \\begin{{aligned}}
            
            \\implies {nPr} 
            & = {nPr_expanded}
            \\\\ \\\\
            
            &\\text {{
            Multiply and divide the expression with missing terms to complete a factorial
            }}
            \\\\ \\\\
            & = {p_fraction}
            \\\\ \\\\
            
            & = {latex(nPr_expr_symb)}
            \\\\ \\\\
            
            & = {latex(nPr_expr)}
            \\\\ \\\\
            
            \\end{{aligned}}
            $"""
            stmts.append(stmt.strip())

        else:
            stmt = f"""
            $\\displaystyle 
            \\begin{{aligned}}
            
            \\implies {nPr} 
            & = {nPr_expanded}
            \\\\ \\\\
            
            & = {latex(nPr_expr_symb)}
            \\\\ \\\\
            
            & = {latex(nPr_expr)}
            \\\\ \\\\
            
            \\end{{aligned}}
            $"""
            stmts.append(stmt.strip())

        return stmts
        pass
    
    
    def get_random_data(self, count = 2):
        x, y = self.x, self.y
        
        vars = []
        while len(vars) < count:
            
            a, b = random.sample([x, y], 2)
            a = random.choice([
                a,
                a * random.randint(1, 5),
                a / random.randint(1, 5),
                a ** random.randint(1, 5)
                ])
            b = random.choice([
                a,
                a / b,
                b / a,
                a * b
                ])
            
            if b == 0:
                continue
            
            b = b * random.choice([1, -1])
            
            vars.append(b)
            pass
        
        n = random.randint(2, 5)
        
        vars.append(n)
        
        return vars
    
    def get_factorial_statements(self, n):
        factorial_latex = self.get_factorial_latex(n)
        
        latex_factorial_n = latex(factorial(ueval(n)))
        
        result = {}
        
        prob_s = f"""
        Write short expression for 
        
        $\\displaystyle {factorial_latex}$
        """
        result['prob_s'] = prob_s

        soln_s = f"""
        $\\displaystyle 
        {factorial_latex} 
        = {latex_factorial_n}
        $
        
        $\\displaystyle 
        {latex_factorial_n}
        $ is read as factorial of ${latex(n)}$ or simply as factorial ${latex(n)}$.
        """
        result['soln_s'] = soln_s

        answer_s = f"""
        $\\displaystyle 
        {latex_factorial_n}
        $
        """
        result['answer_s'] = answer_s
        
        
        return result
        pass
    
    pass
#class BinomialHelper:
