'''
Created on 02-Mar-2021

@author: bthakur
'''
import numpy as np
import random
import inspect
import traceback
from sympy import simplify, sympify, latex, Rational

from xv.km.util import latex_fraction
        
        
from ..helper import BaseHelper
from xv.km.managers import BaseManager
from xv.km.kelements import KProblemTemplate
from xv.util import list_to_str, toIntOrFloat

from xv.km.util import KmTemplateUtil


class PrimeNumberManager(BaseManager):
    '''
    Prime Number

    Minimum Grade: 3
    Maximum Grade: 12


    Examples
    --------
        ke = PrimeNumberManager()
    
        ke.printProblemTypes()
        ke.helpProblemType(0)
    
        ke.getRandomProblem(problem_type = 0)
        ke.getRandomProblem(problem_type = 1)
        ...
        ke.printProblem()
        ke.printAnswer()
        ke.printSolution()

        ke.initProblem(10, 24)
        ke.initProblem(15, 25, 30)


    doc_style: xv_doc
    ''' 

    
    
    def __new__(cls, *args, **kwargs):
        instance = super(PrimeNumberManager, cls).__new__(cls, *args, **kwargs)
        for base_class in PrimeNumberManager.__bases__:
            base_class.__init__(instance, *args, **kwargs)
        return instance
    
    def __init__(self, *args, **kwargs):
        self.base_helper = BaseHelper(**kwargs)
        self.setContent()
        pass

    def setContent(self, *args, **kwargs):
        self._content = f"""
        A prime number is a positive integer that cannot be divided by any other positive integer except 1 and itself.
        The examples are:
            2, 3, 5, 7, 11, 13, 17 ...
        
        The opposite of prime numbers is composite numbers. The examples are:
            4     # divisible by 1, 2 and 4
            6     # divisible by 1, 2, 3 and 6
            8     # divisible by 1, 2, 4 and 8
            9     # divisible by 1, 2, 3 and 9
            
        All even numbers except 2 are composite.
        
        """.strip()
        pass


    def _init_problem_types(self, *args, **kwargs):
        self._append_problem_types([
            '_problem_highest_possible_area',
            '_problem_highest_possible_product',
            '_problem_highest_possible_product_2',
            '_problem_divisibility',
            '_problem_shortest_possible_sum_sides',
            '_problem_list_prime_numbers',
            '_problem_is_prime',
            '_problem_factorize',
            '_problem_prime_factorize',
            '_problem_prime_coprime',
         ])
        pass

    def _problem_highest_possible_area(self, *args, **kwargs):
        """
        Highest possible area

        Minimum Grade: 3
        Maximum Grade: 12

        doc_style: xv_doc
        """
        pnhelper = self.base_helper.get_arithmetic_helper().get_prime_number_helper()
        
        n1 = random.choice(range(2, 10))
        n2 = random.choice(range(3, 10))
        side_tuples = [(x, n1 + n2 - x, x * (n1 + n2 - x)) 
                       for x in range(1, n1 + n2)]
        max_area = max([x[2] for x in side_tuples])
        max_area_data = [x for x in side_tuples
                         if x[2] == max_area][0]
                         
        side_tuples_str = [f"{l} * {w} = {a}"
                           for l, w, a in side_tuples]
        side_tuples_str = list_to_str(side_tuples_str, sep = "\n\n")
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Find the highest possible area of a rectangle if sum of length and width is {n1 + n2} and both are whole numbers.
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')
        
        soln_s = f"""
        <b>Possible areas:</b>

        {side_tuples_str}
        
        Therefore, the maximum area is:
        {max_area_data[0]} * {max_area_data[1]} = {max_area_data[2]}
        
        <b>Observation 1:</b>
        When length and width are closest to each other, the area is the highest.
        
        <b>Observation 2:</b>
        After reaching the highest area, the areas are repeated in reverse order.
        So, we can ignore the repeated rows while solving problems.
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {max_area}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    
    def _problem_highest_possible_product(self, *args, **kwargs):
        """
        Highest possible product of whole numbers

        Minimum Grade: 3
        Maximum Grade: 12

        doc_style: xv_doc
        """
        pnhelper = self.base_helper.get_arithmetic_helper().get_prime_number_helper()
        
        n1 = random.choice(range(2, 10))
        n2 = random.choice(range(3, 10))
        side_tuples = [(x, n1 + n2 - x, x * (n1 + n2 - x)) 
                       for x in range(1, n1 + n2)]
        max_area = max([x[2] for x in side_tuples])
        max_area_data = [x for x in side_tuples
                         if x[2] == max_area][0]
                         
        side_tuples_str = [f"{l} * {w} = {a}"
                           for l, w, a in side_tuples]
        side_tuples_str = list_to_str(side_tuples_str, sep = "\n\n")
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Find the highest product of two whole numbers if their sum is {n1 + n2}.
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')
        
        soln_s = f"""
        <b>Possible product values:</b>

        {side_tuples_str}
        
        Therefore, the maximum product value:
        {max_area_data[0]} * {max_area_data[1]} = {max_area_data[2]}
        
        <b>Observation 1:</b>
        When the numbers are closest to each other, the product is the highest.
        
        <b>Observation 2:</b>
        After reaching the highest product values, the values are repeated in reverse order.
        So, we can ignore the repeated rows while solving problems.
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {max_area}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    def _problem_highest_possible_product_2(self, *args, **kwargs):
        """
        Highest possible product

        Minimum Grade: 3
        Maximum Grade: 12

        doc_style: xv_doc
        """
        
        n1 = random.choice(range(2, 10))
        n2 = random.choice(range(3, 10))
        side = toIntOrFloat((n1 + n2)/2)
        side_latex = latex_fraction(n1 + n2, 2)
        highest_product = toIntOrFloat(side * side)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Find the highest possible product of two numbers if their sum is {n1 + n2}.
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')
        
        soln_s = f"""
        The product of numbers is the highest, when they are closest to each other.
        So, in this case, we the we get highest products when both numbers are equal.
        
        side = $\\displaystyle {side_latex} = {side}$
        
        Therefore, the highest product value 
        
        = {side} * {side}
        
        = {highest_product}
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {highest_product}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass

    def _problem_divisibility(self, *args, **kwargs):
        """
        @generate_notebook: Default True

        doc_style: xv_doc
        """

        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: 
        creating notebook and getting problem from DivisibilityManager
        """
        self._log(sInfo.strip())
        
        
        #create notebook
        generate_notebook = kwargs.get('generate_notebook', True)
        
        if generate_notebook:
            saveToFolder = f"./"
            KmTemplateUtil.CREATE_MANAGER_NOTEBOOK("xv.math.quickmaths", 
                                                   "DivisibilityManager", 
                                                   saveToFolder = saveToFolder,
                                                   overwrite = False, 
                                                   verbose = self.verbose)

        #get a problem        
        from xv.math.quickmaths import DivisibilityManager
        ke = DivisibilityManager(verbose = self.verbose)
        problem_types = [
            '_problem_divisible_by_multiples_of_10',
            '_problem_divisible_by_powers_of_2_5',
            '_problem_divisible_by_2_5',
            '_problem_divisible_by_3_9',
            '_problem_divisible_by_6',
            '_problem_divisible_by_11',
        ]
        
        problem_type = random.choice(problem_types)
        
        kwargs['problem_type'] = problem_type
        kwargs['generate_notebook'] = False
        problemTemplate = ke.getProblemTemplate(*args, **kwargs)
        
        return problemTemplate

    def _problem_shortest_possible_sum_sides(self, *args, **kwargs):
        """
        _problem_shortest_possible_sum_sides

        Minimum Grade: 3
        Maximum Grade: 12

        doc_style: xv_doc
        """
        pnhelper = self.base_helper.get_arithmetic_helper().get_prime_number_helper()
        
        n1 = random.choice(range(2, 10))
        n2 = random.choice(range(2, 10)) * random.choice(range(3, 10))
        area = n1 * n2
 
        factor_pairs = pnhelper.get_factor_pairs(area)
        
        side_tuples_str = [f"{l * w} = {l} * {w}, \t sum of sides = {l} + {w} = {l + w} "
                           for l, w in factor_pairs]
        side_tuples_str = list_to_str(side_tuples_str, sep = "\n\n")
        
        
        shortest_sum = min([l + w for l, w in factor_pairs])
        shortest_pair = [(l, w, l + w) for l, w in factor_pairs
                         if l + w == shortest_sum][0]
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Find the all possible lengths and widths of a rectangle whose area is {n1 * n2}
        and lengths and widths are whole numbers only.
        """.replace("\n", "")
        
        problemTemplate.add_problem(problem_s.strip(), 'text/html')
        
        soln_s = f"""
        <b>Possible areas:</b>

        {side_tuples_str}
        
        <b>Therefore, the shortest sum of sides is:</b>
        {shortest_pair[0]} + {shortest_pair[1]} = {shortest_pair[2]}

        <b>Observation 1:</b>
        When length and width are closest to each other, their sum is the lowest.
        
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {shortest_pair[2]}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass


    def _problem_list_prime_numbers(self, *args, **kwargs):
        """
        List of Prime Numbers


        Minimum Grade: 4
        Maximum Grade: 12


        doc_style: xv_doc
        """

        pnhelper = self.base_helper.get_arithmetic_helper().get_prime_number_helper()
        
        size = 5 + 5 * random.randint(0, 2)
        prime_numbers = pnhelper.getPrimeNumbers(size = size)
        prime_numbers_squares_str = pnhelper.get_prime_numbers_squares_str(size = size)

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Write first {size} prime numbers and square of each of them.
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')
        
        soln_s = f"""
        Prime numbers: {list_to_str(prime_numbers)}.
        
        Squares of prime numbers: 
        {prime_numbers_squares_str}
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
            
        answer_s = f"""
        Prime numbers and their squares: 
        {prime_numbers_squares_str}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass




    def _problem_is_prime(self, *args, **kwargs):
        """
        Is a number a prime number?


        Minimum Grade: 4
        Maximum Grade: 12


    
        Examples
        --------
        from xv.math.algebra import PrimeNumberManager
        ke = PrimeNumberManager()


        ke.getRandomProblem(problem_type = '_problem_is_prime')


        doc_style: xv_doc
        """

        pnhelper = self.base_helper.get_arithmetic_helper().get_prime_number_helper()
        
        pns, p1, cns, n1, possibleFactors, sqrt_n = pnhelper.init_data()

        prime_numbers_squares_str = pnhelper.get_prime_numbers_squares_str(end = n1 ** .5 + 10)

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Is {n1} a prime number?
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')
        
        soln_s = f"""
        Prime numbers and their squares:
        {prime_numbers_squares_str}
        
        The prime numbers whose squares are lower or equal to the given number {n1} can be its factor.
        They are {list_to_str(possibleFactors, empty_list_as = None)}.
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        if n1 == p1:
            nAns = True
            soln_s = f"""
            {n1} is not divisible by any of these prime numbers.
            Therefore, {n1} has no factors other than 1 and itself
            Therefore, {n1} is a prime number.
            """
        else:
            nAns = False
            nFactors = set([x for x in  possibleFactors if n1 % x == 0 ])
            soln_s = f"""
            {n1} is divisible by {list_to_str(nFactors, empty_list_as = None)}.
            Therefore, {n1} has factors other than 1 and itself
            Therefore, {n1} is not a prime number.
            """
            pass
        
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
            
        answer_s = f"""
        {nAns}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
        pass



    
    def _problem_factorize(self, *args, **kwargs):
        """
        Factorize an integer into its prime factors


        Minimum Grade: 4
        Maximum Grade: 12


    
        Examples
        --------
        from xv.math.algebra import PrimeNumberManager
        ke = PrimeNumberManager()


        ke.getRandomProblem(problem_type = '_problem_factorize')


        doc_style: xv_doc
        """

        pnhelper = self.base_helper.get_arithmetic_helper().get_prime_number_helper()
        
        n1, sqrt_n, possibleFactors, nFactors, nfactorSets = pnhelper.init_prime_factor_data()
        prime_numbers_squares_str = pnhelper.get_prime_numbers_squares_str(end = n1 ** .5 + 10)

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Find all factor pairs of {n1}, one of them being prime number.
        Example:\n\tFactor pairs of 12 = [(2, 6), (3,4)]
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')
        
        soln_s = f"""
        Prime numbers and their squares:
        {prime_numbers_squares_str}
        
        The prime numbers whose squares are lower or equal to the given number {n1} can be its factor.
        They are {list_to_str(possibleFactors, empty_list_as = None)}.

        Among them, {n1} is divisible by {list_to_str(nFactors, empty_list_as = None)}.
        
        Therefore, factor pairs of {n1} are \n{list_to_str(nfactorSets, empty_list_as = None)}.
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {list_to_str(nfactorSets, empty_list_as = None)}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
    
        pass


    def _problem_prime_factorize(self, *args, **kwargs):
        """
        Factorize an integer into its prime factors


        Minimum Grade: 4
        Maximum Grade: 12


    
        Examples
        --------
        from xv.math.algebra import PrimeNumberManager
        ke = PrimeNumberManager()


        ke.getRandomProblem(problem_type = '_problem_prime_factorize')


        doc_style: xv_doc
        """

        pnhelper = self.base_helper.get_arithmetic_helper().get_prime_number_helper()
        n1, sqrt_n, possibleFactors, nFactors, nfactorSets = pnhelper.init_prime_factor_data()

        primeFactors = pnhelper.getPrimeFactors_as_str(n1)
        
        primeFactors_latex = str(primeFactors).replace("**", "^")
        
        factor_12_latex = str("2**2 * 3*1").replace("**", "^")

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Express {n1} as product of prime numbers.
        Example:
        Factors pairs of 12 = $\\displaystyle {factor_12_latex}$
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')
        
        soln_s = f"""
        {n1} is less than {sqrt_n**2}, square of {sqrt_n}.
        If {n1} is divisible by a number higher than {sqrt_n}, the quotient of that division must be lower than {sqrt_n}..
        Therefore, in any factor set, one element must be a prime number lower than {sqrt_n}..

        Prime numbers lower than {sqrt_n} are {list_to_str(possibleFactors, empty_list_as = None)}.
        Among them, {n1} is divisible by {list_to_str(nFactors, empty_list_as = None)}.
        
        Therefore, factor pairs of {n1} are:
        $\\displaystyle {primeFactors_latex}$
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        $\\displaystyle {primeFactors_latex}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
    
        pass

    def _problem_prime_coprime(self, *args, **kwargs):
        """
        Co-prime numbers


        Minimum Grade: 4
        Maximum Grade: 12


    
        Examples
        --------
        from xv.math.algebra import PrimeNumberManager
        ke = PrimeNumberManager()


        ke.getRandomProblem(problem_type = '_problem_prime_coprime')


        doc_style: xv_doc
        """

        pnhelper = self.base_helper.get_arithmetic_helper().get_prime_number_helper()
        
        n1 = random.choice(range(0, 1000))
        n2 = random.choice(range(0, 1000))
        possibleFactors1 = pnhelper.getPossibleFactors(n1)
        possibleFactors2 = pnhelper.getPossibleFactors(n2)
        nFactors1 = [x for x in  possibleFactors1 if n1 % x == 0 ]
        nFactors2 = [x for x in  possibleFactors2 if n2 % x == 0 ]
        
        commonFactors = list(set(nFactors1).intersection(set(nFactors2)))
        commonFactors.sort()
        
        if len(commonFactors) > 0:
            isCoprime = False
            isCoprime_str = f"As they have common prime factors, {n1} and {n2} are not co-prime pair."
        else:
            isCoprime = True
            isCoprime_str = f"As they have no common prime factors, {n1} and {n2} are co-prime pair."

        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        problem_s = f"""
        Are {n1} and {n2} co-prime pair?
        Note: A co-prime pair has no common prime factors.
        """
        problemTemplate.add_problem(problem_s.strip(), 'text/html')
        
        soln_s = f"""
        Prime factors of {n1} = {list_to_str(nFactors1, empty_list_as = None)}.
        Prime factors of {n2} = {list_to_str(nFactors2, empty_list_as = None)}.
        
        Common prime factors = {list_to_str(commonFactors, empty_list_as = None)}.
        
        {isCoprime_str}.
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {isCoprime}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
    
        pass
    
    
    
    
