'''
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
                   )
from xv.util import list_to_str, sorted_k_partitions

class PrimeNumberHelper:
    _CACHED_PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23]

    def __init__(self, **kwargs):
        self.verbose = kwargs.get('verbose', False)
        self._primes = PrimeNumberHelper._CACHED_PRIMES
        self.start = None
        self.end = None
        self.size = None
        self._result = None
        self._steps = None

        pass

    def init_data(self):
        pns = self.getPrimeNumbers(start = 2, end = 1000)
        p1 = random.choice(pns)
        
        cns = list(set(range(2, 1000)) - set(pns))
        temp = random.choice(cns)
        
        n1 = random.choice([temp, p1])
        possibleFactors = set(self.getPossibleFactors(n1))
        sqrt_n = int(n1 ** 0.5) + 1
        
        return pns, p1, cns, n1, possibleFactors, sqrt_n
        pass

    def get_common_prime_factors(self, *nums, common_to_all = True):
        prime_factors = []
        for num in nums:
            p_factors = self.getPrimeFactors(num)
            if p_factors:
                prime_factors.extend(list(p_factors.keys()))
            pass
        
        prime_factors = [x for x in prime_factors if x > 1]
        if common_to_all:
            prime_factors = [x for x in set(prime_factors)
                             if prime_factors.count(x) == len(nums)]
        else:
            prime_factors = [x for x in set(prime_factors)
                             if prime_factors.count(x) > 1]
        
        return prime_factors
        pass

    def getPossibleFactors(self, n):
        sqrt_n = int(n ** 0.5) + 1
        possibleFactors = [p for p in self._primes if p < sqrt_n]
        return possibleFactors
        pass
    
    def getPrimeFactors(self, n):
        primeFactors = {}
        tempN = n
        isComposite = True
        while isComposite and tempN != 1:
            isComposite = False
            for p in PrimeNumberHelper._CACHED_PRIMES:
                if tempN % p == 0:
                    primeFactors[p] = primeFactors.get(p, 0) + 1
                    tempN = tempN //p
                    isComposite = True
                    break
                pass
            pass
        if not primeFactors or tempN != 1:
            primeFactors[tempN] = primeFactors.get(tempN, 0) + 1
            
        return primeFactors
        pass
    
    def getPrimeFactors_as_str(self, n, as_latex = False):
        factors = self.getPrimeFactors(n)
        return self.convertPrimeFactors_dict_to_str(factors, as_latex = as_latex)
        pass
 
    def convertPrimeFactors_dict_to_str(self, factors, as_latex = False):
        keys = list(factors.keys())
        keys.sort()
        
        if as_latex:
            exp_factors = [ f"{k} ^ {{{factors[k]}}}" for k in keys]
        else:
            exp_factors = [ f"{k} ** {factors[k]}" for k in keys]

        exp_factors = " \\cdot ".join(exp_factors)
            
        return exp_factors
        pass
 
    def get_factor_pairs(self, n):
        prime_factors = self.getPrimeFactors(n)
        
        factor_items = []
        for k, v in prime_factors.items():
            factor_items.extend([k for i in range(v)])
            pass

        factor_pairs = sorted_k_partitions(factor_items, 2)
        
        factor_pairs = [(reduce(lambda x, y: x * y, list1),
                         reduce(lambda x, y: x * y, list2)) 
                        for list1, list2 in factor_pairs]
        
        factor_pairs = list(set(factor_pairs))
        factor_pairs = sorted(factor_pairs, key=lambda x: x[0], reverse = False)


        return factor_pairs
        pass
    
    def init_prime_factor_data(self, n1 = None):
        if n1 == None:
            n1 = random.choice(range(0, 1000))
        sqrt_n = int(n1 ** 0.5) + 1
        possibleFactors = self.getPossibleFactors(n1)
        nFactors = [x for x in  possibleFactors if n1 % x == 0 ]
        nfactorSets = [ (x, n1//x)
                    for x in nFactors
            ]
        return n1, sqrt_n, possibleFactors, nFactors, nfactorSets
        pass

    def get_prime_numbers_squares_str(self, start = 2, end = None, size = None):
        prime_numbers = self.getPrimeNumbers(start = start, end = end, size = size)
        squares = [f"${x}^{{2}} = {x}\\cdot{x} = {x*x}$" 
                                 for x in prime_numbers]
        
        squares_str = "\n".join(squares)
        return squares_str
         

    def getPrimeNumbers(self, start = 2, end = None, size = None):
        self._initParams(start, end, size)
        self._calcPrime()
        return self._result
    
    def _initParams(self, start, end, size):
        if end is None:
            end = np.inf
            if size is None:
                size = 5

        self.start = start
        self.end = end
        self.size = size
        
        self._result =[]
        self._steps = []
        
        pass
    
    
    def _calcPrime(self):
        if self._primes:
            nMax = max(max(self._primes), 1)
        else:
            nMax = 1
            
        n = min(nMax, self.start)
        
        while n <= self.end:
            n = n + 1
                        
            if not self.size is None:
                if len(self._result) >= self.size:
                    break
            
            self._isPrime(n)
            pass
            
        pass
    
    
    def _isPrime(self, n):
        possibleFactors = self.getPossibleFactors(n)
        for p in possibleFactors:
            if n % p != 0:
                continue
            return None
            pass
        self._appendPrime(n)
        pass

    def _appendPrime(self, n):
        if not n in self._primes:
            self._primes.append(n)
            #print(f"#self._primes = {self._primes}")

        if n >= self.start and n <= self.end:
            if not n in self._result:
                self._result.append(n)
        pass

    def get_highest_prime_factors(self, *nums):
        highest_prime_factors = {}
        for num in nums:
            prime_factors = self.getPrimeFactors(num)
            
            for k,v in prime_factors.items():
                if not k in highest_prime_factors.keys():
                    highest_prime_factors[k] = [v]
                else:
                    highest_prime_factors[k].append(v)
                pass
            pass
        
        highest_prime_factors = {k:max(v)
                                 for k, v in highest_prime_factors.items()}
        return highest_prime_factors
        pass


    def get_lowest_common_prime_factors(self, *nums):
        prime_factor_sets = []
        
        keys = None
        for num in nums:
            prime_factors = self.getPrimeFactors(num)
            prime_factor_sets.append(prime_factors)
            if keys is None:
                keys = list(prime_factors.keys())
            else:
                keys = list(set(keys).intersection(set(prime_factors.keys())))
            pass
        
        if not keys:
            return None
        
        lcp_factors = {}
        for prime_factors in prime_factor_sets:            
            for k,v in prime_factors.items():
                if not k in keys:
                    continue
                
                if not k in lcp_factors.keys():
                    lcp_factors[k] = v
                else:
                    lcp_factors[k] = min(lcp_factors[k], v)
                pass
        
        return lcp_factors
        pass


    def get_prime_factors_exp_str(self, n):
        primeFactors = self.getPrimeFactors(n)
        primeFactors_str = " * ".join([f"{k}^{v}" for k, v in primeFactors.items()])
        return primeFactors_str
        pass


    pass
#class PrimeNumberHelper:
