'''
Created on 10-Jun-2021

@author: bthakur
'''
import random
import inspect
import traceback
import numpy as np
from xv.util import toIntOrFloat
from sympy import symbols, factorial

#from xv.util import UtilClass
from ._arithmetic_helper import ArithmeticHelper

from xv.util.functions._util_functions import list_to_str
from xv.util.htmlutil import get_option_and_answer_statements

class SeriesPatternHelper(ArithmeticHelper):
    def __init__(self, arr = None, **kwargs):
        super().__init__(**kwargs)
        self.set_series_data(arr)
        pass
    
    def set_series_data(self, arr):
        self.arr = arr
        self._chain_arrays = self._init_chain_arrays(arr)
        pass
    
    
    def get_random_array(self):
        arrays = self._get_default_arrays()
        identifier = f"{self.__class__.__name__}{id(self)}.{inspect.stack()[0][3]}"
        arr = self.get_no_repeat_choice(identifier, arrays)
        return arr


    def _get_default_arrays(self):
        m1 = random.randint(1, 5)
        steps = random.randint(1, 5)
        m2 = m1 + random.randint(5, 8) * steps + 1
        range2 = range(m1, m2, steps)
        
        am = np.array(range2) + random.randint(2, 10)
        gm = np.array(range2) * random.randint(2, 5)
        sqrs = np.array(range2) ** 2
        cubes = np.array(range2) ** 3
        
        exp_arrs = []
        for j in [2, 3, 4, 5, 10]:
            exp_arr = [j ** 16]
            for i in range(1, 4):
                exp_arr.append(toIntOrFloat(exp_arr[-1] ** 0.5))
            exp_arrs.append(exp_arr)
        
        sorted_am = list(np.sort(am))
        sorted_gm = list(np.sort(gm))
        two_series1 = []
        two_series2 = []
        for i in range(len(sorted_am)):
            two_series1.append(sorted_am[i])
            two_series1.append(sorted_gm[i])
            
            two_series2.append(sorted_gm[i])
            two_series2.append(sorted_am[i])
            pass
        
        arrays = [
            two_series1,
            two_series2,
            list(np.sort(am)),
            list(np.sort(am) + np.sort(gm)),
            list(np.sort(am) + np.sort(sqrs)),
            list(np.sort(am) + np.sort(cubes)),
            list(np.sort(gm)),
            list(np.sort(gm) + np.sort(sqrs)),
            list(np.sort(gm) + np.sort(cubes)),
            
            list(np.sort(sqrs)),
            list(np.sort(sqrs) + np.sort(cubes)),

            list(np.sort(cubes)),
            
            [1, 27, 125, 343, 729],
            [4, 9, 16, 25],
            
            [324, 361, 400, 441],
            [512, 729, 1000],
            [4, 36, 324, 2916],
            [1, 3, 6, 10, 15],
            [10, 22, 46, 94, 190],
            [1, 4, 8, 11, 22, 25],
            [1, 2, 6, 36, 44, 440],
            [3, 4, 8, 10, 13, 16 ],
            [32, 40, 24, 16, 24],
            [1, 2, 6, 24],
            [1, 27, 125, 343, 729],
            [12, 18, 10, 6, 14, 5, 0, 10],
            [6, 3, 10, 14, 15],
            [4,7, 10, 13],
            [25, 23, 21, 19, 17, 15],
            [2187, 729, 243, 81],
            [1, 3, 6, 10, 15, 21],
            [0, 1, 1, 2, 3, 5, 8, 13],
            [3, 4, 8, 10, 13, 16],
            [10, 22, 46, 94, 190],
            [2, 1, (1/2), (1/4)],
            [4, 18, 60, 186, 564],
            [7, 27, 93, 301, 915, 2775, 8361],
            [38, 49, 62, 72, 77, 91, 101],
            [7055, 7223, 7393, 7565],
            [2184, 2730, 3360, 4080, 4896],
            
        ]
        
        
        arrays.extend(exp_arrs)
        return arrays
        pass
    
    
    def get_alternate_arrays(self):
        arr = self.arr
        arr1 = [ arr[i] for i in range(0, len(arr), 2) ]
        arr2 = [ arr[i] for i in range(1, len(arr), 2) ]
        return [SeriesPatternHelper(arr1), SeriesPatternHelper(arr2)]
        pass
   
    def get_statement(self):
        if self._chain_arrays is None:
            return None
        
        delta = symbols(u'\U00000394')
        s = ""
        for arr_idx, arr_dict in enumerate(self._chain_arrays):
            arr = arr_dict.get('arr', None)
            if arr is None:
                continue
            
            if arr_idx == 0:
                s = s + "\n" + f"$Series:$\t {arr}"
            else:
                s = s + "\n" + f"${delta}_{arr_idx}:$\t {arr}"

                            
            next_term = self.get_next_term(arr_dict)
            if not next_term is None:
                s = s + "\n\t\t\t" + f"next_term: {next_term}"
                
            for k, v in arr_dict.items():
                if k.startswith('is_'):
                    s = s + "\n\t\t\t" + f"{k}: {v}"
                    
                    if k == 'is_am_series':
                        s = s + "\t" + f"common_difference: {arr_dict['common_difference']}"
                        pass
                    if k == 'is_gm_series':
                        s = s + "\t" + f"common_ratio: {arr_dict['common_ratio']}"
                        pass
                    if k == 'is_exp_series':
                        s = s + "\t" + f"common_log: {arr_dict['common_log']}"
                        pass
                    
                    
                    pass
                pass
            
            s = s + "\n"
            pass
        return s
   
    def get_chain_arrays(self):
        return self._chain_arrays
    
    def get_next_term(self, arr_dict = None):
        if arr_dict is None:
            if self._chain_arrays is None:
                return None
            arr_dict = self._chain_arrays[0]
        
        next_term = arr_dict.get('next_term', None)
        if not next_term is None:
            return next_term
        
        next_term = arr_dict.get('computed_term', None)
        if not next_term is None:
            return next_term
        
        return None
        pass
    
    def _init_chain_arrays(self, arr):
        if arr is None:
            return None
        
        #init chain_arrays
        temp = self.arr
        diff_arrays = [self.get_arr_patterns(temp)]
        
        while len(temp) > 1:
            diff_dict = self.get_diff_dict(temp)
            
            #compute terms 
            next_term = diff_dict.get('next_term', None)
            if not next_term is None:
                for idx in range(len(diff_arrays)-1, -1, -1):
                    next_term = diff_arrays[idx]['arr'][-1] + next_term
                    diff_arrays[idx]['computed_term'] = next_term
                    pass
                pass
            
            diff_arrays.append(diff_dict)
            temp = diff_dict['arr']
            pass
        
        return diff_arrays
        pass        
    
    
    def get_diff_dict(self, arr = None):
        if arr is None:
            arr = self.arr
        
        diff_arr = [ arr[i+1] - arr[i]   
                     for i in range(len(arr)-1)]
        
        diff_dict = self.get_arr_patterns(diff_arr)
        return diff_dict
        pass
    
    def get_arr_patterns(self, arr):
        
        arr_dict = {'arr': arr}
        if self._update_arr_dict(arr, arr_dict, 'is_am_series'):
            arr_dict['common_difference'] = toIntOrFloat(arr[1] - arr[0])
            return arr_dict
        
        if self._update_arr_dict(arr, arr_dict, 'is_gm_series'):
            arr_dict['common_ratio'] = toIntOrFloat(arr[1]/arr[0])
            return arr_dict
        
        if self._update_arr_dict(arr, arr_dict, 'is_exp_series'):
            arr_dict['common_log'] = toIntOrFloat(np.log(arr[1]) / np.log(arr[0]))
            return arr_dict
        
        if self._update_arr_dict(arr, arr_dict, 'is_fibonacci_series'):
            return arr_dict
        
        if self._update_arr_dict(arr, arr_dict, 'is_perfect_squares'):
            return arr_dict
        
        if self._update_arr_dict(arr, arr_dict, 'is_perfect_cubes'):
            return arr_dict
        return arr_dict
        pass
    
    def _update_arr_dict(self, arr, arr_dict, test_name):
        test_method = getattr(self, test_name)
        test_result = test_method(arr)
        
        if test_result:
            arr_dict[test_name] = test_result[0]
            arr_dict['next_term'] = test_result[1]
            return True
        
        return False    
        pass

    def is_am_series(self, arr = None):
        if arr is None:
            arr = self.arr
        
        if len(arr) < 3:
            return False    
        
        prev_diff = None
        for i in range(len(arr)-1):
            if prev_diff is None:
                prev_diff = arr[i+1] - arr[i]
                continue
            
            curr_diff = arr[i+1] - arr[i]
            if prev_diff != curr_diff:
                return False
            pass
        
        next_term = arr[-1] + curr_diff 
        return True, toIntOrFloat(next_term)
        pass
    
    def is_gm_series(self, arr = None):
        if arr is None:
            arr = self.arr
        
        if len(arr) < 3:
            return False    
        
        prev_ratio = None
        for i in range(len(arr)-1):
            if arr[i] == 0:
                return False
            if prev_ratio is None:
                prev_ratio = arr[i+1] / arr[i]
                continue
            
            curr_ratio = arr[i+1] / arr[i]
            if prev_ratio != curr_ratio:
                return False
            pass
        
        next_term = arr[-1] * curr_ratio 
        return True, toIntOrFloat(next_term)
        pass

    def is_exp_series(self, arr = None):
        if arr is None:
            arr = self.arr
        
        if len(arr) < 3:
            return False    
        
        prev_log_factor = None
        curr_log_factor = None
        for i in range(len(arr)-1):
            if arr[i] <= 0 or arr[i+1] <= 0:
                return False
            
            if arr[i] == 1:
                return False
            
            if prev_log_factor is None:
                prev_log_factor = np.log(arr[i+1]) / np.log(arr[i])
                continue
            
            curr_log_factor = np.log(arr[i+1]) / np.log(arr[i])
            if prev_log_factor != curr_log_factor:
                return False
            pass
        
        next_term = arr[-1] ** curr_log_factor 
        return True, toIntOrFloat(next_term)
        pass


    def is_fibonacci_series(self, arr = None):
        if arr is None:
            arr = self.arr
        
        if len(arr) < 4:
            return False    
        
        for i in range(2, len(arr)):
            if arr[i-2] + arr[i-1]  != arr[i]:
                return False
            pass
        
        next_term = arr[-2] + arr[-1]
        return True, toIntOrFloat(next_term)
        pass
    

    def is_perfect_squares(self, arr = None):
        if arr is None:
            arr = self.arr
        
        if len(arr) < 3:
            return False    
        
        sqrts = []
        for i in range(len(arr)):
            if arr[i] < 0:
                return False

            sqrt_val = np.sqrt(arr[i])
            sqrts.append(sqrt_val)

            sout = f"""
            arr[{i}] = {arr[i]}
            sqrt_val = {sqrt_val}
            round(sqrt_val, 0) = {round(sqrt_val, 0)}
            abs(sqrt_val - round(sqrt_val, 0)) = {abs(sqrt_val - round(sqrt_val, 0))}
            abs(sqrt_val - round(sqrt_val, 0)) > 1e-10 = {abs(sqrt_val - round(sqrt_val, 0)) > 1e-10}
            """
            #print(sout)
            
            if abs(sqrt_val - round(sqrt_val, 0)) > 1e-10:
                return False
            else:
                #print(sout)
                pass
            
            pass
        
        #TODO Fix it
        next_term = round((sqrts[-1] + (sqrts[-1] - sqrts[-2])) ** 2, 0)
        return True, toIntOrFloat(next_term)
        pass
    
    def is_perfect_cubes(self, arr = None):
        if arr is None:
            arr = self.arr
        
        if len(arr) < 3:
            return False    
        
        cube_roots = []
        for i in range(len(arr)):
            if arr[i] < 0:
                return False

            cube_root_val = arr[i] ** (1/3)
            cube_roots.append(cube_root_val)
            
            sout = f"""
            arr = {arr[i]}
            cube_root_val = {cube_root_val}
            round(cube_root_val, 0) = {round(cube_root_val, 0)}
            abs(cube_root_val - round(cube_root_val, 0)) = {abs(cube_root_val - round(cube_root_val, 0))}
            is_perfect_cubes = {abs(cube_root_val - round(cube_root_val, 0)) > 1e-10}
            """
            
            if abs(cube_root_val - round(cube_root_val, 0)) > 1e-10:
                return False
            else:
                #print(sout)
                pass
            pass

        #TODO Fix it
        next_term = round((cube_roots[-1] + (cube_roots[-1] - cube_roots[-2])) ** 3, 0)
        return True, toIntOrFloat(next_term)
        pass
    
    def get_problem_find_next_term_advanced(self, **kwargs):
        '''
        '''
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())

        index = kwargs.get('index', None)
        
        try:
            methods = [
                self.find_next_simple_arithmetic_series_term,
                self.find_next_compound_arithmetic_series_term,
                self.find_next_simple_geometric_series_term,
                self.find_next_simple_fibonacci_series_term,
                self.find_next_simple_square_series_term,
                self.find_next_simple_cubic_series_term,
                self.find_next_simple_triangular_series_term,
                self.find_next_simple_prime_number_series_term,
                self.find_next_simple_factorial_series_term,
                
                ]
            
            if ( index is None or 
                 index >= len(methods) or
                 - index > len(methods) 
               ):
                random_choices = list(range(len(methods)))
        
                identifier = f"{self.__class__.__name__}{id(self)}.{inspect.stack()[0][3]}.random_choices"
                random_choice = self.get_no_repeat_random_choice(identifier, random_choices)
            else:
                random_choice = index

            
            method = methods[random_choice]
            return method(**kwargs)
        
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}: {e}
            kwargs = {kwargs}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            return None
            pass
        pass


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


        first_term = random.randint(1, 10)  * random.choice([1, -1])
        common_diff = random.randint(1, 10) * random.choice([1, -1])
        n = 5
        n_terms = [first_term + idx * common_diff for idx in range(n)]
        
        n_terms_str = list_to_str(n_terms, sep = ", ")
        
        next_term = n_terms[-1] + common_diff
        
        correct_option = next_term
        
        option_range = [1, 2, 3, common_diff, common_diff * 2, common_diff * 3]
        
        options = [n_terms[-1] + x for x in option_range]
        
        options = [x for x in options if x != correct_option and x != n_terms[-1]]
        
        options = random.sample(options, 3)
        
        options.append(correct_option)

        option_s, answer_s = get_option_and_answer_statements(options, correct_option)
        
        prob_s = f"""
        What is the next number in the series: {n_terms_str}, ___?
        {option_s}
        """
        
        soln_stmts = []

        if common_diff > 0:
            soln_stmt = f"""
            This is an arithmetic sequence where each term increases by {common_diff}.  
            {n_terms[0]} + {common_diff} = {n_terms[1]}  
            {n_terms[1]} + {common_diff} = {n_terms[2]}  
            {n_terms[2]} + {common_diff} = {n_terms[3]}  
            {n_terms[3]} + {common_diff} = {n_terms[4]}  
            {n_terms[4]} + {common_diff} = {n_terms[4] + common_diff}  
            """
        else:
            abs_common_diff = abs(common_diff)
            soln_stmt = f"""
            This is an arithmetic sequence where each term decreases by {abs_common_diff}.  
            {n_terms[0]} - {abs_common_diff} = {n_terms[1]}  
            {n_terms[1]} - {abs_common_diff} = {n_terms[2]}  
            {n_terms[2]} - {abs_common_diff} = {n_terms[3]}  
            {n_terms[3]} - {abs_common_diff} = {n_terms[4]}  
            {n_terms[4]} - {abs_common_diff} = {n_terms[4] + common_diff}  
            """
            
        soln_stmts.append(soln_stmt.strip())
        
        
        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


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


        first_term = random.randint(1, 10)  * random.choice([1, -1])
        common_diff = random.randint(1, 10) * random.choice([1, -1])
        addl_common_diff = random.randint(1, 10) * random.choice([1, -1])
         
        
        n = 5
        n_terms_extended = []
        n_terms_extended_str = []
        
        for idx in range(n + 1):
            if idx == 0:
                term = first_term
                term_str = f"& {first_term}"
                revised_diff = common_diff
            else:
                term = first_term + idx * revised_diff
                
                if revised_diff == common_diff:
                    term_str = f"""
                    & {n_terms_extended[-1]} 
                    & + 
                    & \\left( {revised_diff} \\right)
                    & = {n_terms_extended[-1]} 
                    & + 
                    & \\large{{ \\color{{red}}{{ {revised_diff} }} }}
                    & = {term}
                    """
                else:
                    term_str = f"""
                    & {n_terms_extended[-1]} 
                    & + 
                    & \\left( 
                      \\color{{red}}{{ {revised_diff - addl_common_diff} }}
                      + {addl_common_diff} 
                      \\right)
                    & = {n_terms_extended[-1]} 
                    & + 
                    & \\large{{ \\color{{red}}{{ {revised_diff} }} }}
                    & = {term}
                    """
                    
                revised_diff = revised_diff + addl_common_diff
                pass

            n_terms_extended.append(term)
            n_terms_extended_str.append(f"{term_str}")
            pass
        
        n_terms_extended_str = list_to_str(n_terms_extended_str, sep = f"\\\\ \\\\")
        
        n_terms = n_terms_extended[:-1]
        
        n_terms_str = list_to_str(n_terms, sep = ", ")
        
        next_term = n_terms[-1] + common_diff
        
        correct_option = next_term
        
        option_range = [1, 2, 3, common_diff, common_diff * 2, common_diff * 3]
        
        options = [n_terms[-1] + x for x in option_range]
        
        options = [x for x in options if x != correct_option and x != n_terms[-1]]
        
        options = random.sample(options, 3)
        
        options.append(correct_option)

        option_s, answer_s = get_option_and_answer_statements(options, correct_option)
        
        prob_s = f"""
        What is the next number in the series: {n_terms_str}, ___?
        {option_s}
        """
        
        soln_stmts = []

        soln_stmt = f"""
        This is an compound arithmetic sequence where the difference between terms changes by $
        {addl_common_diff}$.  
        
        $\\displaystyle 
        \\begin{{array}}{{rllll}}
        
        {n_terms_extended_str}  
        
        \\end{{array}}
        $
        """
            
        soln_stmts.append(soln_stmt.strip())
        
        
        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
    
    
    def find_next_simple_geometric_series_term(self, **kwargs):
        '''
        '''
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        first_term = random.randint(1, 10)  * random.choice([1, -1])
        common_ratio = random.choice([random.randint(2, 10), random.randint(1, 10) * 10])
        common_ratio = common_ratio * random.choice([1, -1])
        
        n = 4
        n_terms = [first_term * common_ratio ** idx for idx in range(n)]
        
        n_terms_str = list_to_str(n_terms, sep = ", ")
        
        next_term = n_terms[-1] * common_ratio
        
        correct_option = next_term
        
        options = [correct_option - common_ratio, correct_option - common_ratio,
                   correct_option + common_ratio, correct_option + common_ratio,
                   correct_option * common_ratio, correct_option * common_ratio ** 2,
                   ]
        
        options = [x for x in options if x != correct_option and x != n_terms[-1]]
        
        options = random.sample(options, 3)
        
        options.append(correct_option)

        option_s, answer_s = get_option_and_answer_statements(options, correct_option)
        
        prob_s = f"""
        What is the next number in the series: {n_terms_str}, ___?
        {option_s}
        """
        
        soln_stmts = []

        soln_stmt = f"""
        This is a geometric sequence where each term is multiplied by {common_ratio}.  
        {n_terms[0]} $\\times$ {common_ratio} = {n_terms[1]}  
        {n_terms[1]} $\\times$ {common_ratio} = {n_terms[2]}  
        {n_terms[2]} $\\times$ {common_ratio} = {n_terms[3]}  
        {n_terms[3]} $\\times$ {common_ratio} = {n_terms[3] * common_ratio}  
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        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
    
    def find_next_simple_fibonacci_series_term(self, **kwargs):
        '''
        '''
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        first_term = random.randint(1, 10) * random.choice([1, -1])
        
        n_terms = [first_term, first_term]
        for idx in range(3):
            n_terms.append(n_terms[-2] + n_terms[-1])
            pass
        
        n_terms_str = list_to_str(n_terms, sep = ", ")
        
        next_term = n_terms[-2] + n_terms[-1]
        
        correct_option = next_term
        
        options = [correct_option - 1, correct_option - 2,

                   (n_terms[-1] + n_terms[-1]) // 2,
                   
                   n_terms[-2] * 2 + n_terms[-1],
                   n_terms[-2] + n_terms[-1] * 2,
                   
                   n_terms[-2] * 3 - n_terms[-1],
                   - n_terms[-2] + n_terms[-1] * 2,
                   ]
        
        options = [x for x in options if x != correct_option and x != n_terms[-1]]
        
        options = random.sample(options, 3)
        
        options.append(correct_option)

        option_s, answer_s = get_option_and_answer_statements(options, correct_option)
        
        prob_s = f"""
        What is the next number in the series: {n_terms_str}, ___?
        {option_s}
        """
        
        soln_stmts = []

        soln_stmt = f"""
        This is the Fibonacci sequence, where each term is the sum of the two preceding terms.  
        {n_terms[0]}
        {n_terms[1]}
        {n_terms[0]} + {n_terms[1]} = {n_terms[2]}
        {n_terms[1]} + {n_terms[2]} = {n_terms[3]}
        {n_terms[2]} + {n_terms[3]} = {n_terms[4]}
        {n_terms[3]} + {n_terms[4]} = {n_terms[3] + n_terms[4]}
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        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

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


        first_seed_term = random.randint(1, 10) * random.choice([1, -1])
        
        seed_terms = []
        for idx in range(5):
            seed_terms.append(first_seed_term + idx)
            pass
        
        n_terms = [x**2 for x in seed_terms[:-1]]
        
        n_terms_str = list_to_str(n_terms, sep = ", ")
        
        next_term = seed_terms[-1] ** 2
        
        correct_option = next_term
        
        options = [n_terms[-1] + n_terms[-2],
                   n_terms[-1] * n_terms[-2],
                   
                   n_terms[-1] * 2,
                   n_terms[-1] * 3,
                   
                   correct_option - 1,
                   correct_option - 2,
                   correct_option + 1,
                   correct_option + 2,
                   
                   ]
        
        options = [x for x in options if x != correct_option and x != n_terms[-1]]
        
        options = random.sample(options, 3)
        
        options.append(correct_option)

        option_s, answer_s = get_option_and_answer_statements(options, correct_option)
        
        prob_s = f"""
        What is the next number in the series: {n_terms_str}, ___?
        {option_s}
        """
        
        soln_stmts = []

        soln_stmt = f"""
        These are perfect squares of numbers.   
        ${seed_terms[0]}^2 = {seed_terms[0]} \\times {seed_terms[0]} = {n_terms[0]}$
        ${seed_terms[1]}^2 = {seed_terms[1]} \\times {seed_terms[1]} = {n_terms[1]}$
        ${seed_terms[2]}^2 = {seed_terms[2]} \\times {seed_terms[2]} = {n_terms[2]}$
        ${seed_terms[3]}^2 = {seed_terms[3]} \\times {seed_terms[3]} = {n_terms[3]}$
        ${seed_terms[4]}^2 = {seed_terms[4]} \\times {seed_terms[4]} = {next_term}$
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        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
    
    def find_next_simple_cubic_series_term(self, **kwargs):
        '''
        '''
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        first_seed_term = random.randint(1, 10) * random.choice([1, -1])
        
        seed_terms = []
        for idx in range(5):
            seed_terms.append(first_seed_term + idx)
            pass
        
        n_terms = [x**3 for x in seed_terms[:-1]]
        
        n_terms_str = list_to_str(n_terms, sep = ", ")
        
        next_term = seed_terms[-1] ** 3
        
        correct_option = next_term
        
        options = [n_terms[-1] + n_terms[-2] + n_terms[-3],
                   n_terms[-1] * n_terms[-2] * n_terms[-3],
                   
                   n_terms[-1] * 3,
                   n_terms[-2] * 3,
                   
                   correct_option - 1,
                   correct_option - 2,
                   correct_option + 1,
                   correct_option + 2,
                   
                   ]
        
        options = [x for x in options if x != correct_option and x != n_terms[-1]]
        
        options = random.sample(options, 3)
        
        options.append(correct_option)

        option_s, answer_s = get_option_and_answer_statements(options, correct_option)
        
        prob_s = f"""
        What is the next number in the series: {n_terms_str}, ___?
        {option_s}
        """
        
        soln_stmts = []

        soln_stmt = f"""
        These are perfect cubes of numbers.   
        ${seed_terms[0]}^3 = {seed_terms[0]} \\times {seed_terms[0]} \\times {seed_terms[0]} = {n_terms[0]}$
        ${seed_terms[1]}^3 = {seed_terms[1]} \\times {seed_terms[1]} \\times {seed_terms[1]} = {n_terms[1]}$
        ${seed_terms[2]}^3 = {seed_terms[2]} \\times {seed_terms[2]} \\times {seed_terms[2]} = {n_terms[2]}$
        ${seed_terms[3]}^3 = {seed_terms[3]} \\times {seed_terms[3]} \\times {seed_terms[3]} = {n_terms[3]}$
        ${seed_terms[4]}^3 = {seed_terms[4]} \\times {seed_terms[4]} \\times {seed_terms[4]} = {next_term}$
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        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

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


        first_seed_term = random.randint(1, 10) * random.choice([1, -1])
        
        seed_terms = []
        for idx in range(7):
            seed_terms.append(first_seed_term + idx)
            pass
        
        n_terms_extended = []
        for idx in range(len(seed_terms)):
            if idx == 0:
                continue
            
            term = ( seed_terms[idx - 1] * seed_terms[idx] ) // 2
            n_terms_extended.append(term)
            pass
        
        n_terms = n_terms_extended[:-1]
        
        n_terms_str = list_to_str(n_terms, sep = ", ")
        
        next_term = n_terms_extended[-1]
        
        correct_option = next_term
        
        options = [n_terms[-1] + n_terms[-2],
                   n_terms[-1] * n_terms[-2],
                   
                   n_terms[-1] * 2,
                   n_terms[-1] * 3,
                   
                   correct_option - 1,
                   correct_option - 2,
                   correct_option + 1,
                   correct_option + 2,
                   
                   ]
        
        options = [x for x in options if x != correct_option and x != n_terms[-1]]
        
        options = random.sample(options, 3)
        
        options.append(correct_option)

        option_s, answer_s = get_option_and_answer_statements(options, correct_option)
        
        prob_s = f"""
        What is the next number in the series: {n_terms_str}, ___?
        {option_s}
        """
        
        soln_stmts = []

        soln_stmt = f"""
        These are triangular numbers calculated by $\\displaystyle \\frac{{ n (n + 1) }}{{ 2 }}$.    
        $\\displaystyle \\frac {{ {seed_terms[0]} \\times {seed_terms[1]} }} {{ 2 }} = {n_terms[0]} $
        
        $\\displaystyle \\frac {{ {seed_terms[1]} \\times {seed_terms[2]} }} {{ 2 }} = {n_terms[1]} $
        
        $\\displaystyle \\frac {{ {seed_terms[2]} \\times {seed_terms[3]} }} {{ 2 }} = {n_terms[2]} $
        
        $\\displaystyle \\frac {{ {seed_terms[3]} \\times {seed_terms[4]} }} {{ 2 }} = {n_terms[3]} $
        
        $\\displaystyle \\frac {{ {seed_terms[4]} \\times {seed_terms[5]} }} {{ 2 }} = {n_terms[4]} $
        
        $\\displaystyle \\frac {{ {seed_terms[5]} \\times {seed_terms[6]} }} {{ 2 }} = {n_terms_extended[-1]} $
        
        """ 
        soln_stmts.append(soln_stmt.strip())
        
        
        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

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


        pnhelper = self.get_prime_number_helper()
        
        size = random.randint(6, 30)
        
        prime_numbers = pnhelper.getPrimeNumbers(size = size)
        
        n_terms = prime_numbers[-6:-2]
        
        n_terms_str = list_to_str(n_terms, sep = ", ")
        
        next_term = prime_numbers[-2]
        
        correct_option = next_term
        
        options = [correct_option - 1, correct_option - 2,
                   correct_option + 1, correct_option + 2,

                   n_terms[-1] - 1, n_terms[-1] - 2,
                   n_terms[-1] + 1, n_terms[-1] + 2,

                   (n_terms[-1] + n_terms[-2]) // 2,
                   
                   prime_numbers[-1],
                   ]
        
        options = [x for x in options if x != correct_option and x != n_terms[-1]]
        
        options = random.sample(options, 3)
        
        options.append(correct_option)

        option_s, answer_s = get_option_and_answer_statements(options, correct_option)
        
        prob_s = f"""
        What is the next number in the series: {n_terms_str}, ___?
        {option_s}
        """
        
        soln_stmts = []

        soln_stmt = f"""
        These are consecutive prime numbers.  
        The prime number next to {n_terms[-1]} is {correct_option}.
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        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
    
    def find_next_simple_factorial_series_term(self, **kwargs):
        '''
        '''
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        first_seed_term = random.randint(1, 10)
        
        seed_terms = []
        for idx in range(6):
            seed_terms.append(first_seed_term + idx)
            pass
        
        n_terms_extended = []
        n_terms_extended_desc = []
        
        
        for x in seed_terms:
            n_terms_extended.append( factorial(x) )
            
            x_range = list(range(x, 0, -1))
            x_range_str = list_to_str(x_range, sep = f" \\times ")
            n_terms_extended_desc.append( f"{x_range_str}" )
            pass
            
        n_terms = n_terms_extended[:-1]
        n_terms_str = list_to_str(n_terms, sep = ", ")
        
        next_term = factorial(seed_terms[-1])
        
        correct_option = next_term
        
        options = [n_terms[-1] + n_terms[-2],
                   n_terms[-1] * n_terms[-2],
                   
                   n_terms[-1] * 2,
                   n_terms[-1] * 3,
                   
                   correct_option - 1,
                   correct_option - 2,
                   correct_option + 1,
                   correct_option + 2,
                   
                   ]
        
        options = [x for x in options if x != correct_option and x != n_terms[-1]]
        
        options = random.sample(options, 3)
        
        options.append(correct_option)

        option_s, answer_s = get_option_and_answer_statements(options, correct_option)
        
        prob_s = f"""
        What is the next number in the series: {n_terms_str}, ___?
        {option_s}
        """
        
        soln_stmts = []
        

        soln_stmt = f"""
        These are factorials of numbers. 
          
        ${seed_terms[0]}! = {n_terms_extended_desc[0]} = {n_terms[0]}$
        ${seed_terms[1]}! = {n_terms_extended_desc[1]} = {n_terms[1]}$
        ${seed_terms[2]}! = {n_terms_extended_desc[2]} = {n_terms[2]}$
        ${seed_terms[3]}! = {n_terms_extended_desc[3]} = {n_terms[3]}$
        ${seed_terms[4]}! = {n_terms_extended_desc[4]} = {n_terms[4]}$
        ${seed_terms[5]}! = {n_terms_extended_desc[5]} = {n_terms_extended[-1]}$
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        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 SeriesPatternHelper