'''
Created on 07-Feb-2021

@author: bthakur
'''
import numpy as np

        
class Prime():
    _CACHED_PRIMES = []
    def __init__(self):
        self._result = None
        self._primes = Prime._CACHED_PRIMES
        self._steps = None
        
        self.start = None
        self.end = None
        self.size = None
        
        pass
    pass

    def _prime(self, start = 2, end = None, size = None, show = True):
        self._initProb(start, end, size)
        self._calcPrime()
        if show:
            self.printResult(self._steps)
        return self._result
    
    
    def _calcPrime(self):
        
        if self._primes:
            nMax = max(max(self._primes), 1)
        else:
            nMax = 1
            
        n = min(nMax, self.start)
        #print(f"Starting at {n}")
        
        while n <= self.end:
            n = n + 1
                        
            if not self.size is None:
                if len(self._result) >= self.size:
                    break
            
            sHint = f'\nIs {n} a prime number?'
            step = {'hint': sHint }
            self._appendStep(step, n)

            self._isPrime(n)
            pass
            
        step = {'expr': self._result, 'hint': self._getProbStatement() }
        self._appendStep(step)

        pass
    
    def _isPrime(self, n):
        possibleFactors = self._getPossibleFactors(n)
        
        for p in possibleFactors:
            if n % p != 0:
                continue
            
            sHint = f"As {n} is divisible by {p}, it is not a prime number."
            step = {'hint': sHint }
            self._appendStep(step, n)
            return None
            pass
        
        if n >= self.start and n <= self.end:
            sqrt_n = round(n ** 0.5, 1)
            sHint = f"\n{n} is a prime number."
            sHint = f"{sHint}\nIt is not divisible by prime numbers "
            sHint = f"{sHint}less than or equal to its square root {sqrt_n}:\n{possibleFactors}"
            step = {'hint': sHint }
            self._appendStep(step)
            
        self._appendPrime(n)
        pass
    

    def _isVerbose(self, n = None):
        if n is None:
            return True
        
        if len(self._steps) > 20:
            step = {'hint': '....' }
            if self._steps[-1] != step:
                self._appendStep(step)
            return False
        
        if self.size > 5:
            if self.start > 1000:
                return False
            
            if abs(self.end - self.start) > 50:
                return False
            
            return False

        if self.start > 1000:
            return False
            
        if n >= self.start and n <= self.end:
            return True
        
        return False
        pass
    
    def _getProbStatement(self):
        if self.end != np.inf:
            if self.size is None:
                sHint = f"""Finding prime numbers between {self.start} and {self.end}"""
            else:
                sHint = f"""Finding {self.size} prime numbers between {self.start} and {self.end}"""
            pass
            return sHint
            
        if self.size is None:
            sHint = f"""Finding prime numbers >= {self.start}"""
        else:
            sHint = f"""Finding {self.size} prime numbers >= {self.start}"""
        
        return sHint


    def _appendStep(self, data, n = None):
        if self._isVerbose(n):
            self._steps.append(data)
        pass

    def _initProb(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 = []
        
        self._appendStep({'hint': self._getProbStatement() })
        
        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 _getPossibleFactors(self, n):
        sqrt_n = int(n ** 0.5) + 1
        possibleFactors = [p for p in self._primes if p < sqrt_n]

        sHint = f"Square root of {n} is less than {sqrt_n}."
        step = {'hint': sHint }
        self._appendStep(step, n)
        
        sHint = f"The prime numbers smaller than {sqrt_n} are:\n{possibleFactors}"
        step = {'hint': sHint }
        self._appendStep(step, n)
        
        
        sHint = f"So, we will check whether {n} is divisible by any of the above prime numbers"
        step = {'hint': sHint }
        self._appendStep(step, n)

        return possibleFactors
        
        pass
    
    def _intToStr(self, n):
        if n >= 0:
            return f"{n}"
        
        return f"({n})"
        pass
  
    def _math(self, x):
        return x
        pass
  
    def printResult(self, result):
        try:
            from IPython.display import display, Math
        except:
            display = print()
            Math = self._math
        
        for row in result:
            if not isinstance(row, dict):
                display(Math(
                    row
                ))
                continue
            
                
            hint = row.get('hint', None)
            if not hint is None:
                print(hint)
        
            expr_latex = row.get('expr_latex', None)
            if not expr_latex is None:
                display(Math(expr_latex))
        
            expr = row.get('expr', None)
            if not expr is None:
                display(Math(str(expr)))
        
             

