'''
Created on 12-Jul-2021

@author: bthakur
'''
import random
import inspect
import traceback
from functools import reduce

import numpy as np

from sympy import (symbols, Symbol, pi, latex,
                   Rational, Derivative,
                   UnevaluatedExpr,
                   Mul
                   )
from xv.util import list_to_str, sorted_k_partitions

from ...util import MathUtil
from ._prime_number_helper import PrimeNumberHelper

# LcmHcfHelper

from xv.util import UtilClass

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

    def get_hcf_solution(self, *nums, rule = None, size = None):
        if not nums and size is None:
            size = random.randint(2, 4)
            
        if not nums:
            nums = self.get_random_integer_series(size = size)
        
        if len(nums) < 2:
            nums = self.get_random_integer_series(size = size)
        
        if rule is None:
            rule = random.choice(['prime_factors',
                                  'common_divisors',
                                  ])
            
        if rule == 'prime_divisors':
            return self.get_hcf_solution_prime_factors(*nums)
            pass
        
        if rule == 'common_divisors':
            return self.get_hcf_solution_common_divisors(*nums)
            pass
        
        return None
        pass

    
    def get_lcm_solution(self, *nums, rule = None, size = None):
        if not nums and size is None:
            size = random.randint(2, 4)
            
        if not nums:
            nums = self.get_random_integer_series(size = size)
        
        if len(nums) < 2:
            nums = self.get_random_integer_series(size = size)
        
        if rule is None:
            rule = random.choice(['integer_multiples',
                                  'prime_factors',
                                  'common_divisors',
                                  ])
            
        if rule == 'integer_multiples':
            return self.get_lcm_solution_integer_multiples(*nums)
            pass
        
        if rule == 'prime_factors':
            return self.get_lcm_solution_prime_factors(*nums)
            pass
        
        if rule == 'common_divisors':
            return self.get_lcm_solution_common_divisors(*nums)
            pass
        
        return None
        pass

    def get_lcm_solution_integer_multiples(self, *nums):

        answer = MathUtil.lcm(*nums)
        size = max([answer // x for x in nums]) + 1

        nums_str = list_to_str(nums, last_sep = " and ")

        problem_s = f"""
        Find LCM of {nums_str}.
        """
        soln_items = []

        soln_item = f"""
        LCM of {nums_str}
        """
        soln_items.append(soln_item.strip())

        for num in nums:
            num_data = self.get_random_multiples_data(num, size)
            soln_item = f"""
            Multiples of {num}
            $\\displaystyle = {num_data['n_table_str']}$
            $\\displaystyle = {num_data['n_multiples_str']}$
            """
            soln_items.append(soln_item.strip())
        
        soln_item = f"""
        As you see, the first non-zero common number in all multiple tables is $\\displaystyle 
        {answer}$. So,
        
        LCM of {nums_str} = $\\displaystyle {answer}$
        """
        soln_items.append(soln_item.strip())

        
        answer_s = f"""
        $\\displaystyle {answer}$
        """
        
        soln_s = "\n\n".join(soln_items)
        
        result = { 
            'nums': nums,
            'nums_str': nums_str,
            'answer': answer,
            'problem_s': problem_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        return result
        pass

    def get_hcf_solution_prime_factors(self, *nums):

        answer = MathUtil.hcf(*nums)
        size = max([answer // x for x in nums]) + 1

        nums_str = list_to_str(nums, last_sep = " and ")


        problem_s = f"""
        Find HCF of {nums_str}.
        
        Note: Highest Common Factor (HCF) is also called Greatest Common Divisor (GCD).
        """
        soln_items = []

        soln_item = f"""
        HCF of {nums_str}:
        """
        soln_items.append(soln_item.strip())

        pn_helper = PrimeNumberHelper(verbose = self.verbose)

        prime_factors_dict = {}
        for num in nums:
            prime_factors = pn_helper.getPrimeFactors_as_str(num, as_latex = True)
            prime_factors_dict[num] = prime_factors
            
            soln_item = f"""
            Prime factors of $\\displaystyle {num} = {prime_factors}$
            """
            soln_items.append(soln_item.strip())
            pass

        lcp_factors = pn_helper.get_lowest_common_prime_factors(*nums)
        
        if not lcp_factors:
            hcf_computed = 1
            soln_item = f"""
            As ther are no common factors to all numbers, the HCF is {hcf_computed}.
            
            HCF of {nums_str} $\\displaystyle = {hcf_computed}$
            """
            soln_items.append(soln_item.strip())
            pass
        else:
            lcp_factors_str = [f"{k}^{v}" for k, v in lcp_factors.items()]
            lcp_factors_str = " * ".join(lcp_factors_str)
            
            hcf_computed = [k**v for k, v in lcp_factors.items()]
            hcf_computed = Mul(*hcf_computed)
            
            soln_item = f"""
            The HCF will be product of lowest common exponents from all numbers.
            
            HCF of {nums_str} 
            
            $\\displaystyle = {lcp_factors_str}$
            
            $\\displaystyle = {hcf_computed}$
            
            """
            soln_items.append(soln_item.strip())
            pass
                
        answer_s = f"""
        $\\displaystyle {answer}$
        """
        
        soln_s = "\n\n".join(soln_items)
        
        result = { 
            'nums': nums,
            'nums_str': nums_str,
            'answer': answer,
            'problem_s': problem_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        return result
        pass


    def get_lcm_solution_prime_factors(self, *nums):

        answer = MathUtil.lcm(*nums)
        size = max([answer // x for x in nums]) + 1

        nums_str = list_to_str(nums, last_sep = " and ")


        problem_s = f"""
        Find LCM of {nums_str}.
        """
        soln_items = []

        soln_item = f"""
        LCM of {nums_str}:
        """
        soln_items.append(soln_item.strip())

        pn_helper = PrimeNumberHelper(verbose = self.verbose)

        prime_factors_dict = {}
        for num in nums:
            prime_factors = pn_helper.getPrimeFactors_as_str(num, as_latex = True)
            prime_factors_dict[num] = prime_factors
            
            soln_item = f"""
            Prime factors of $\\displaystyle {num} = {prime_factors}$
            """
            soln_items.append(soln_item.strip())
            pass

        highest_factors = pn_helper.get_highest_prime_factors(*nums)
        highest_factors_str = [f"{k}^{v}" for k, v in highest_factors.items()]
        highest_factors_str = " * ".join(highest_factors_str)
        
        lcm_computed = [k**v for k, v in highest_factors.items()]
        lcm_computed = Mul(*lcm_computed)
        
        soln_item = f"""
        The LCM will be product all prime factors with highest exponents from all numbers.
        
        LCM of {nums_str} 
        
        $\\displaystyle = {highest_factors_str}$
        
        $\\displaystyle = {lcm_computed}$
        
        """
        soln_items.append(soln_item.strip())

        
        answer_s = f"""
        $\\displaystyle {answer}$
        """
        
        soln_s = "\n\n".join(soln_items)
        
        result = { 
            'nums': nums,
            'nums_str': nums_str,
            'answer': answer,
            'problem_s': problem_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        return result
        pass

    def get_hcf_solution_common_divisors(self, *nums):
        pn_helper = PrimeNumberHelper(verbose = self.verbose)

        answer = MathUtil.hcf(*nums)
        size = max([answer // x for x in nums]) + 1

        nums_str = list_to_str(nums, last_sep = " and ")


        problem_s = f"""
        Find HCF of {nums_str}.
        """
        soln_items = []

        soln_item = f"""
        HCF of {nums_str}:
        
        """
        soln_items.append(soln_item.strip())

        common_prime_factors = pn_helper.get_common_prime_factors(*nums,
                                                          common_to_all = True)

        if (not common_prime_factors):
            hcf_computed = 1
            soln_item = f"""
            As ther are no common factors to all numbers, the HCF is {hcf_computed}.
            
            HCF of {nums_str} $\\displaystyle = {hcf_computed}$
            """
            soln_items.append(soln_item.strip())
            pass
        else:
            sub_soln_items = self._compute_hcf_by_common_divisors(*nums)
            soln_items.extend(sub_soln_items)
            pass

        
        answer_s = f"""
        $\\displaystyle {answer}$
        """
        
        soln_s = "\n\n".join(soln_items)
        
        result = { 
            'nums': nums,
            'nums_str': nums_str,
            'answer': answer,
            'problem_s': problem_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        return result
        pass

    def _compute_hcf_by_common_divisors(self, *nums):
        soln_items = []
        
        pn_helper = PrimeNumberHelper(verbose = self.verbose)

        soln_item = f"""
        To get HCF, we will divide numbers with common prime divisors. 
        There must be a common prime divisor to all numbers. We write quotient of numbers below them.
        We continue this till we have a common prime divisor to all numbers.
        """
        soln_items.append(soln_item.strip())


        temp_nums = [x for x in nums]
        html_table = []
        html_table.append(f"""
        <style>
            table.hcf_table {{
              border-collapse: collapse;
            }}
            
            table.hcf_table td{{
              padding: 5px 10px 5px 10px ;
            }}
            
            
            tr.hcf_table {{
              border-bottom: 1pt solid black;
            }}
            td.hcf_table {{
              border-right: 1pt solid black;
            }}
        </style>
        
        
        <table class="hcf_table">
        """)
        
        hcf_args = []
        while True:
            common_prime_factors = pn_helper.get_common_prime_factors(*temp_nums,
                                                              common_to_all = True)
            sOut = f"""
            temp_nums = {temp_nums}
            common_prime_factors = {common_prime_factors}
            """
            self._log(sOut)

            temp_nums_str = [f"<td>{x}</td>" for x in temp_nums]
            temp_nums_str = "".join(temp_nums_str)

            if not common_prime_factors:
                html_row = f"""<tr><td></td>{temp_nums_str}</tr>"""
                html_table.append(html_row)
                break
            
            min_common_prime_factor = min(common_prime_factors)
            hcf_args.append(min_common_prime_factor)
            
            html_row = f"""
            <tr class="hcf_table">
            <td class="hcf_table">{min_common_prime_factor}</td>
            {temp_nums_str}
            </tr>"""
            html_table.append(html_row)

            temp_nums = [x//min_common_prime_factor 
                         if x % min_common_prime_factor == 0  else x 
                         for x in temp_nums]
            
            pass
        html_table.append("</table>")
        
        soln_item = " ".join(html_table)
        soln_item = soln_item.replace("\n", "")
        soln_items.append(soln_item.strip())


        hcf_args_str = [str(x) for x in hcf_args]
        hcf_args_str = " * ".join(hcf_args_str)
        
        hcf_computed = Mul(*hcf_args)

        nums_str = list_to_str(nums, last_sep = " and ")
        
        if hcf_args_str.strip() == f"{hcf_computed}".strip():
            hcf_computed_str = ""
        else:
            hcf_computed_str = f"= {hcf_computed}"
        
        soln_item = f"""
        To compute HCF, we have to multiply all common prime divisors.
        
        Therefore, HCF of {nums_str}
        = {hcf_args_str}
        {hcf_computed}
        """
        soln_items.append(soln_item.strip())

        return soln_items
        pass

    def get_lcm_solution_common_divisors(self, *nums):
        pn_helper = PrimeNumberHelper(verbose = self.verbose)

        answer = MathUtil.lcm(*nums)
        size = max([answer // x for x in nums]) + 1

        nums_str = list_to_str(nums, last_sep = " and ")


        problem_s = f"""
        Find LCM of {nums_str}.
        """
        soln_items = []

        soln_item = f"""
        LCM of {nums_str}:
        
        To get LCM, we will divide numbers with common prime divisors. 
        There must be a common prime divisor to at least two numbers.
        If number is divisible, we write quotient below and if not, the same number.
        
        We continue this till we have a common prime divisor to at least two numbers.
        """
        soln_items.append(soln_item.strip())


        temp_nums = [x for x in nums]
        html_table = []
        html_table.append(f"""
        <style>
            table.lcm_table {{
              border-collapse: collapse;
            }}
            
            table.lcm_table td{{
              padding: 5px 10px 5px 10px ;
            }}
            
            
            tr.lcm_table {{
              border-bottom: 1pt solid black;
            }}
            td.lcm_table {{
              border-right: 1pt solid black;
            }}
        </style>
        
        
        <table class="lcm_table">
        """)
        
        lcm_args = []
        while True:
            common_prime_factors = pn_helper.get_common_prime_factors(*temp_nums,
                                                              common_to_all = False)
            sOut = f"""
            temp_nums = {temp_nums}
            common_prime_factors = {common_prime_factors}
            """
            #self._log(sOut)

            temp_nums_str = [f"<td>{x}</td>" for x in temp_nums]
            temp_nums_str = "".join(temp_nums_str)

            if not common_prime_factors:
                html_row = f"""<tr><td></td>{temp_nums_str}</tr>"""
                html_table.append(html_row)
                
                lcm_args.extend(temp_nums)
                break
            
            min_common_prime_factor = min(common_prime_factors)
            lcm_args.append(min_common_prime_factor)
            
            html_row = f"""
            <tr class="lcm_table">
            <td class="lcm_table">{min_common_prime_factor}</td>
            {temp_nums_str}
            </tr>"""
            html_table.append(html_row)

            temp_nums = [x//min_common_prime_factor 
                         if x % min_common_prime_factor == 0  else x 
                         for x in temp_nums]
            
            pass
        html_table.append("</table>")
        
        soln_item = " ".join(html_table)
        soln_item = soln_item.replace("\n", "")
        soln_items.append(soln_item.strip())


        lcm_args_str = [str(x) for x in lcm_args]
        lcm_args_str = " * ".join(lcm_args_str)
        
        lcm_computed = Mul(*lcm_args)
        
        soln_item = f"""
        To compute LCM, we have to multiply all common prime divisors and the residual numbers.
        
        Therefore, LCM of {nums_str}
        = {lcm_args_str}
        = {lcm_computed}
        """
        soln_items.append(soln_item.strip())

        
        answer_s = f"""
        $\\displaystyle {answer}$
        """
        
        soln_s = "\n\n".join(soln_items)
        
        result = { 
            'nums': nums,
            'nums_str': nums_str,
            'answer': answer,
            'problem_s': problem_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        return result
        pass

    def get_random_integer_series(self, size = 2):
        nums = []
        while len(nums) < size:
            n = random.randint(2, 10)
            if n in nums:
                continue
            nums.append(n)
            pass
        return nums
        pass

    def get_random_numbers(self, size = 2):
        pn_helper = PrimeNumberHelper(verbose = self.verbose)
        
        nums1 = self.get_random_hcf_numbers(size = 3)
        nums2 = self.get_random_hcf_numbers(size = 3)
        nums3 = pn_helper.getPrimeNumbers(end = 50)
        nums = []
        nums.extend(nums1)
        nums.extend(nums2)
        nums.extend(random.sample(nums3, 3))
        
        if len(nums) <= size:
            return nums
        return random.sample(nums, size)
        pass

    def get_random_lcm_numbers(self, size = 2):
        nums = self.get_random_integer_series(size = size)
        
        nums = np.array(nums)
        while random.choice([True, False]):
            fctr = random.randint(2, 12)
            if random.choice([True, False]):
                nums = nums * fctr
            else:
                idx = random.randint(0, len(nums))
                nums[idx] = nums[idx] * fctr
            pass
        nums = list(nums)
        return nums
        pass

    def get_random_hcf_numbers(self, size = 2):
        nums = self.get_random_integer_series(size = size)
        
        nums = np.array(nums)
        while random.randint(0, 2) != 0:
            fctr = random.randint(2, 12)
            if random.choice([True, False]):
                nums = nums * fctr
            else:
                idx = random.randint(0, len(nums) - 1)
                nums[idx] = nums[idx] * fctr
            pass
        nums = list(nums)
        return nums
        pass

    def get_random_multiples_data(self, n, size, include_zero = False):
        if include_zero:
            int_arr = np.arange(0, size)
        else:
            int_arr = np.arange(1, size + 1)
            
        n_multiples = int_arr * n
        
        n_multiples_str = [f"{x}" for x in n_multiples]
        n_multiples_str = ", ".join(n_multiples_str)
        
        n_table_str = [f"{i + 1}*{n}" for i, x in enumerate(n_multiples)]
        n_table_str = ", ".join(n_table_str)
        
        n_table_latex = [f"{i + 1} * {n} = {x} \\\\" for i, x in enumerate(n_multiples)]
        n_table_latex = "".join(n_table_latex)
        
        result = { 
            'n': n,
            'size': size,
            'n_multiples': n_multiples,
            'n_multiples_str': n_multiples_str,
            'n_table_str': n_table_str,
            'n_table_latex': n_table_latex,
            }
        
        return result
        pass
    
    
    def get_problem_lcm_prime_number_abstract_algebra(self):    
        
        LCM, GCD = symbols('LCM, GCD')
        N, m, n = symbols('N, m, n')
        p1, p2, pk = symbols('p_1, p_2, p_k')
        A1, A2, Ak, Ai = symbols('A_1, A_2, A_k, A_i')
        a1, a2, ak, ai = symbols('a_1, a_2, a_k, a_i')
        b1, b2, bk, bi = symbols('b_1, b_2, b_k, b_i')
        
        N_latex = f"""
        {latex(p1)}^{{{latex(A1)}}}  
        {latex(p2)}^{{{latex(A2)}}} 
        \\ldots 
        {latex(pk)}^{{{latex(Ak)}}} 
        """
        
        m_latex = f"""
        {latex(p1)}^{{{latex(a1)}}}  
        {latex(p2)}^{{{latex(a2)}}} 
        \\ldots 
        {latex(pk)}^{{{latex(ak)}}} 
        """
        
        n_latex = f"""
        {latex(p1)}^{{{latex(b1)}}}  
        {latex(p2)}^{{{latex(b2)}}} 
        \\ldots 
        {latex(pk)}^{{{latex(bk)}}} 
        """
        
        LCM_latex = f"""
        {latex(p1)}^{{ \\text{{ max }} \\left(  {latex(a1)}, {latex(b1)} \\right) }}  
        {latex(p2)}^{{ \\text{{ max }} \\left(  {latex(a2)}, {latex(b2)} \\right) }}  
        \\ldots 
        {latex(pk)}^{{ \\text{{ max }} \\left(  {latex(ak)}, {latex(bk)} \\right) }}  
        """
        
        GCD_latex = f"""
        {latex(p1)}^{{ \\text{{ min }} \\left(  {latex(a1)}, {latex(b1)} \\right) }}  
        {latex(p2)}^{{ \\text{{ min }} \\left(  {latex(a2)}, {latex(b2)} \\right) }}  
        \\ldots 
        {latex(pk)}^{{ \\text{{ min }} \\left(  {latex(ak)}, {latex(bk)} \\right) }}  
        """
        
        cond_a_latex = f"""
        0 \\leqslant {latex(a1)} \\leqslant {latex(A1)}, 
        0 \\leqslant {latex(a2)} \\leqslant {latex(A2)}, 
        \\ldots, 
        0 \\leqslant {latex(ak)} \\leqslant {latex(Ak)}
        """
         
        cond_b_latex = f"""
        0 \\leqslant {latex(b1)} \\leqslant {latex(A1)}, 
        0 \\leqslant {latex(b2)} \\leqslant {latex(A2)}, 
        \\ldots, 
        0 \\leqslant {latex(bk)} \\leqslant {latex(Ak)}
        """
         
        cond_int_latex = f"""
        {latex(Ai)}, {latex(ai)}, {latex(bi)} \in \mathbb{{Z}}
        """
         
        prob_stmts = []
        soln_stmts = []

        prob_stmt = f"""

        Let the decomposition of a (positive integral) number ${latex(N)}$ 
        into prime factors be of the form
        <br/>
        <br/>
        $\\displaystyle 
        {latex(N)} = \\Large {{ {N_latex} }}
        $
        """
        prob_stmts.append(prob_stmt.replace("\n", " ").strip())


        prob_stmt = f"""
        Then any two divisors $m$ and $n$ of this number can be written as
        <br/>
        <br/>
        $$\\displaystyle 
        \\begin{{aligned}}
        
        {latex(m)} & = \\Large {{ {m_latex} }}
        \\\\ \\\\
        
        {latex(n)} & = \\Large {{ {n_latex} }}
        \\\\ \\\\
        
        \\text {{ where }} \\quad 
        & \\large {{ {cond_a_latex} }} ,
        \\\\ \\\\
        
        & \\large {{ {cond_b_latex} }} 
        \\quad \\text {{ and }}
        \\\\ \\\\

        & \\large {{ {cond_int_latex} }} 
        
        \\end{{aligned}}
        $$
        """
        prob_stmts.append(prob_stmt.replace("\n", " ").strip())


        prob_stmt = f"""
        Write the expression for 
        ${latex(LCM)}$ and ${latex(GCD)}$ ($HCF$) 
        of ${latex(m)}$ and ${latex(n)}$.
        """
        prob_stmts.append(prob_stmt.replace("\n", " ").strip())


        soln_stmt = f"""
        The LCM of numbers is product of the highest power of each of prime factors.
        
        $$\\displaystyle 
        \\begin{{aligned}}
        
        {latex(LCM)} ( {latex(m)}, {latex(n)} )
        
        & = {latex(LCM)} \\left( 
        \\Large{{ \\left( {m_latex} \\right), \\left( {n_latex} \\right) }}
        \\right) 
        \\\\ \\\\
        
        
        & = \\Large{{ {LCM_latex} }}  
        
        \\end{{aligned}}
        $$
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        The GCD of numbers is product of the lowest power of each of prime factors.
        
        $$\\displaystyle 
        \\begin{{aligned}}
        
        {latex(GCD)} ( {latex(m)}, {latex(n)} )
        
        & = {latex(GCD)} \\left( 
        \\Large{{ \\left( {m_latex} \\right), \\left( {n_latex} \\right) }}
        \\right) 
        \\\\ \\\\
        
        & = \\Large{{ {GCD_latex} }}  
        
        \\end{{aligned}}
        $$
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        prob_s = "\n\n".join(prob_stmts)
        soln_s = "\n\n".join(soln_stmts)
        
        answer_s = f"""
        ${latex(LCM)} = \\Large{{ {LCM_latex} }}$
        ${latex(GCD)} = \\Large{{ {GCD_latex} }}$
        """
        
        result = { 
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        return result
        pass


    pass
#class LcmHcfHelper:
