'''
Created on 12-Jul-2021

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

import numpy as np
import sympy as sp


from sympy import (symbols, Symbol, pi, latex,
                   Rational, Derivative,
                   nsimplify, sympify, simplify,
                   Add,
                   UnevaluatedExpr as ueval
                   )
from xv.util import list_to_str
from xv.km.util import latex_fraction, latex_color
from xv.util import html_table

class PositionalValueHelper:
    _NUM_BASES = list('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ')

    def __init__(self, **kwargs):
        self.verbose = kwargs.get('verbose', False)
        pass


    def get_random_num(self, 
                        n_base = 10, 
                        n_digits = None,
                        decimal_digits = 0 ):
        
        if n_digits is None:
            n_digits = random.randint(2, 5)
        
        num_dict = []
        for i in range(n_digits):
            digit = random.choice(range(0, n_base))
            num_dict.append([i, digit])
            pass
        
        num_dict.reverse()
        
        decimal_dict = []
        if decimal_digits:
            for i in range(1, decimal_digits + 1):
                digit = random.choice(range(0, n_base))
                decimal_dict.append([-i, digit])
                pass
            pass
        
        num = list_to_str([x for i, x in num_dict], sep = "")
        if decimal_dict:
            decimal_part = list_to_str([x for i, x in decimal_dict], sep = "")
            num = f"{num}.{decimal_part}"
            
            num_dict.extend(decimal_dict)
            pass

        num_base10 = sum([x * n_base ** i for i, x in num_dict])
        
        num_latex = f"\\left({num}\\right)_{{{n_base}}}"
        num_base10_latex = f"\\left({num_base10}\\right)_{{10}}"
        
        random_num_data = {
            'n_base': n_base,
            'n_digits': n_digits,
            'decimal_digits': decimal_digits,
            'num': num,
            'num_dict': num_dict,
            'num_latex': num_latex,
            'num_base10': num_base10,
            'num_base10_latex': num_base10_latex,
            }
        return random_num_data
        pass

    def get_random_positional_values(self, 
                                     n_base = 10, 
                                     n_digits = None,
                                     decimal_digits = 0 ):

        random_num_data = self.get_random_num(n_base = n_base, 
                                              n_digits = n_digits,
                                              decimal_digits = decimal_digits)
        
        num_dict = random_num_data['num_dict']
        
        pos_values_latex = [f"{x} * {n_base}^{{{i}}}" 
                      for i, x in num_dict]
        
        pos_values = []
        for i, x in num_dict:
            if i >= 0:
                pos_value = latex(x * n_base ** i)
            else:
                pos_value = latex_fraction(x, n_base ** -i )
                
            pos_values.append(pos_value)
            pass
        
        pos_values_latex_str = list_to_str(pos_values_latex, " + ")
        pos_values_str = list_to_str(pos_values, " + ")
        
        if n_base != 10:
            pos_values_str = f"\\left({{{pos_values_str}}}\\right)_{{10}}"
            pos_values_latex_str = f"\\left({{{pos_values_latex_str}}}\\right)_{{10}}"

        
        result = {
            'pos_values_latex': pos_values_latex,
            'pos_values': pos_values,
            'pos_values_latex_str': pos_values_latex_str,
            'pos_values_str': pos_values_str,
            }
        result.update(random_num_data)
        return result
    
    def convert_digit_base_to_10(self, base_val, n_base):
        num_bases = self.__class__._NUM_BASES[:n_base]
        digit = num_bases.index(base_val)
        return digit
        pass

    def convert_digit_10_to_base(self, decimal_val, n_base):
        num_bases = self.__class__._NUM_BASES[:n_base]
        digit_str = num_bases[decimal_val]
        try:
            return int(digit_str) 
        except:
            return Symbol(digit_str) 
        pass

    def get_random_recurring_decimal_num(self, 
                        n_base = 10, 
                        int_digits = None,
                        decimal_digits = 2 ):
        
        if int_digits is None:
            int_digits = random.randint(2, 5)
        
        recu_digits = random.randint(1, decimal_digits)
        non_recu_digits = decimal_digits - recu_digits
        
        
        int_dict = []
        for i in range(int_digits):
            if i == 0:
                digit_idx = random.choice(range(1, n_base))
            else:
                digit_idx = random.choice(range(0, n_base))
            digit = self.convert_digit_10_to_base(digit_idx, n_base)
            int_dict.append([int_digits - i - 1, digit])
            pass
        
        
        non_recu_dict = []
        for i in range(1, non_recu_digits + 1):
            digit_idx = random.choice(range(0, n_base))
            digit = self.convert_digit_10_to_base(digit_idx, n_base)
            non_recu_dict.append([-i, digit])
            pass
        
        recu_dict = []
        for i in range(non_recu_digits + 1, decimal_digits + 1):
            if i == decimal_digits:
                digit_idx = random.choice(range(1, n_base))
            else:
                digit_idx = random.choice(range(0, n_base))

            digit = self.convert_digit_10_to_base(digit_idx, n_base)
            recu_dict.append([-i, digit])
            pass

        
        int_part = list_to_str([x for i, x in int_dict], sep = "")
        non_recu_part = list_to_str([x for i, x in non_recu_dict], sep = "")
        recu_part = list_to_str([x for i, x in recu_dict], sep = "")
        
        recu_latex = f"\\overline {{{latex(recu_part)}}}"
        num_latex = f"\\left({latex(int_part)}.{latex(non_recu_part)}{recu_latex}\\right)_{{{n_base}}}"
        
        
        random_num_data = {
            'n_base': n_base,
            'int_digits': int_digits,
            'decimal_digits': decimal_digits,
            'recu_digits': recu_digits,
            'non_recu_digits': non_recu_digits,
            'int_part': int_part,
            'non_recu_part': non_recu_part,
            'recu_part': recu_part,
            'recu_latex': recu_latex,
            'num_latex': num_latex,
            }
        return random_num_data
        pass


    def get_problem_conver_base_10_to_base_n(self, **kwargs):
        '''
        @num_base_10: integer on base 10 to be converted
        @base_n: new integer base of the number num_base_10
        '''
        
        
        num_base_10 = kwargs.get('num_base_10', None)
        base_n = kwargs.get('base_n', None)
        
        if num_base_10 is None:
            num_base_10 = random.randint(1, 10**4)
            
        if base_n is None:
            base_n = random.choice([random.randint(2, 9), random.randint(11, 16)])


        digits_list = list('0123456789ABCDEFGHIJKLMN')
        digits_dict = {i:digits_list[i] for i in range(base_n)}
            
        dividend = num_base_10
        
        conversion_data = []
        num_base_n_list_rev = []
        while True:
            remainder = dividend % base_n
            num_base_n_list_rev.append(digits_dict[remainder])
            
            conversion_data.append([base_n, dividend, remainder, digits_dict[remainder]])

            dividend = dividend // base_n
            
            if dividend == 0:
                break
            
            pass
        
        conversion_data.append(['', 0, '', ''])
        
        num_base_n_list = num_base_n_list_rev[::-1]
        
        num_base_n = "".join(num_base_n_list)
        num_base_n = num_base_n.lstrip('0')
        num_base_n = Symbol(f"\\text{{{num_base_n}}}")
        
        prob_items = []
        soln_items = []
        
        prob_item = f"""
        Write $\\displaystyle \\left( {latex(num_base_10)} \\right)_{{ 10 }}
        $ on base $\\displaystyle {latex(base_n)} $ and verify it.
        """
        prob_items.append(prob_item.strip())
        
        header = ['Divisor', 
                  'Dividend<br/>(Quotient)', 
                  'Remainder', 
                  'Remainder<br/>Symbol'
                  ]
        html_str = html_table(conversion_data, header = header)
        
        
        digits_dict_str = [f"${latex(k)}: {latex(v)}$" for k, v in digits_dict.items()]
        digits_dict_str = "\n".join(digits_dict_str)
        
        soln_item = f"""
        In a base $\\displaystyle {latex(base_n)} 
        $ number system, the permissible digits with their respective new symbols are:
        {digits_dict_str}

        Let us do a series division of $\\displaystyle {latex(num_base_10)} 
        $ by $\\displaystyle {latex(base_n)} $.
        
        {html_str}
        """
        soln_items.append(soln_item.strip())
        
        
        soln_item = f"""
        Let us get the remainder symbol in reverse order, $\\displaystyle {latex(num_base_n)}
        $, which is the new number on $\\displaystyle {latex(base_n)} $.
        
        $\\displaystyle 
        \\left( {latex(num_base_10)} \\right)_{{ 10 }}
        = \\left( {latex(num_base_n)} \\right)_{{ {latex(base_n)} }}
        $
        """
        soln_items.append(soln_item.strip())
        
        pos_values_ueval = []
        pos_values = []
        for i, x in enumerate(num_base_n_list_rev):
            pos_value_ueval = Symbol(f""" {latex(x)} \\cdot {{{latex(base_n)} }}^{{{latex(i)} }} """)
            
            pos_value = int(x) * base_n ** i
            
            pos_values_ueval.append(pos_value_ueval)
            pos_values.append(pos_value)
            pass
        
        pos_values_ueval = pos_values_ueval[::0-1]
        pos_values = pos_values[::-1]
        
        pos_values_ueval_ltx = list_to_str(pos_values_ueval, " + "  , to_latex = True)
        pos_values_ltx = list_to_str(pos_values, " + "  , to_latex = True)
        
        pos_values_sum = sp.Add(*pos_values)

        soln_item = f"""
        Let us verify the solution:
        
        $\\displaystyle 
        \\begin {{aligned}}
        
        \\left( {latex(num_base_n)} \\right)_{{ {latex(base_n)} }}
        
        & = \\left( {pos_values_ueval_ltx} \\right)_{{ {latex(10)} }}
        \\\\ \\\\
        
        & = \\left( {pos_values_ltx} \\right)_{{ {latex(10)} }}
        \\\\ \\\\

        & = \\left( {pos_values_sum} \\right)_{{ {latex(10)} }}

        \\end {{aligned}}
        $
        """
        soln_items.append(soln_item.strip())
        
        
        answer_s = f"""
        ${latex(num_base_n)}$
        """
        
        prob_s = "\n\n".join(prob_items)
        soln_s = "\n\n".join(soln_items)
        
        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        return result
        pass



    def get_recurring_decimals(self, 
                                 n_base = 10, 
                                 int_digits = None,
                                 decimal_digits = 2 ):

        random_num_data = self.get_random_recurring_decimal_num(n_base = n_base, 
                                                    int_digits = int_digits,
                                                    decimal_digits = decimal_digits)
        

        
        soln_items = []

        n_base = random_num_data['n_base']
        decimal_digits = random_num_data['decimal_digits']
        non_recu_digits = random_num_data['non_recu_digits']
        recu_digits = random_num_data['recu_digits']
        
        int_part = random_num_data['int_part']
        non_recu_part = random_num_data['non_recu_part']
        recu_part = random_num_data['recu_part']
        recu_latex = random_num_data['recu_latex']
        num_latex = random_num_data['num_latex']
        
        non_recu_factor = n_base ** ueval(non_recu_digits)
        recu_factor = n_base ** ueval(recu_digits)
        
        num_as_integer = f"{int_part}{non_recu_part}{recu_part}"
        non_recu_as_integer = f"{int_part}{non_recu_part}"
        
        y = Symbol('y')
        
        soln_item = f"""
        $\\displaystyle 
        {num_latex}
        $ 

        Let $\\displaystyle 
        {latex(y)} = {num_latex}
        $ 
        
        """
        soln_items.append(soln_item.strip())
        
        
        soln_item = f"""
        Shift the decimal to $\\displaystyle {non_recu_digits}$ places towords recurring part.
        
        $\\displaystyle 
        \\implies {latex(non_recu_factor)} \\; {latex(y)} 
        = {latex(non_recu_factor)} \\; {num_latex}
        $ 
        """
        soln_items.append(soln_item.strip())
        
        num_1_str = f"{non_recu_as_integer}.{recu_latex}"
        num_1_latex = f"\\left({num_1_str}\\right)_{{{n_base}}}"
        
        soln_item = f"""
        $\\displaystyle 
        \\implies {latex(non_recu_factor)} \\; {latex(y)} 
        = {num_1_latex}  \\qquad \\qquad \\cdots (i)
        $ 
        """
        soln_items.append(soln_item.strip())
        
        num_2_str = f"{num_as_integer}.{recu_latex}"
        num_2_latex = f"\\left({num_2_str}\\right)_{{{n_base}}}"
        
        soln_item = f"""
        Shift the decimal one recurring cycle right.
        
        $\\displaystyle 
        \\implies 
        {latex(non_recu_factor)} \\cdot {latex(recu_factor)} 
        \\; {latex(y)} 
        = {latex(recu_factor)} \\; {num_1_latex}
        $ 

        $\\displaystyle 
        \\implies 
        {latex(non_recu_factor)} \\cdot {latex(recu_factor)} 
        \\; {latex(y)} 
        = {num_2_latex} \\qquad \\qquad \\cdots (ii)
        $ 
        """
        soln_items.append(soln_item.strip())
        
        num_3_parts_latex = f"\\left({num_2_str} - {num_1_str}\\right)_{{{n_base}}}"
        num_4_parts_latex = f"\\left({num_as_integer} - {non_recu_as_integer}\\right)_{{{n_base}}}"
        
        #num_4 = num_as_integer - non_recu_as_integer
        num_4_latex = f""" 
        \\left({num_as_integer} \\right)_{{{n_base}}}
        - \\left({non_recu_as_integer} \\right)_{{{n_base}}}
        """
        
        net_factor_latex = f"{latex(non_recu_factor)} \\left( {latex(recu_factor)} - 1 \\right)"
        net_factor = sympify(simplify(non_recu_factor) * (simplify(recu_factor) - 1))
        
        soln_item = f"""
        Let us do $\\displaystyle 
        \\qquad \\qquad (ii) - (i)
        $ 

        $\\displaystyle 
        \\implies
        {latex(non_recu_factor)} \\cdot {latex(recu_factor)} \\; {latex(y)}
        - 
        {latex(non_recu_factor)} \\; {latex(y)}
        = {num_3_parts_latex} 
        $ 
        """
        soln_items.append(soln_item.strip())


        std_formula = f"""
        {net_factor_latex}
        {latex(y)} 
        = {num_4_parts_latex} 
        \\qquad \\qquad \\text{{This is short cut formula.}}
        """
        std_formula = latex_color(std_formula.strip(), 'red')
        
        soln_item = f"""
        Cancel the decimal part on the right side.

        $\\displaystyle 
        \\implies {std_formula}
        $ 
        """
        soln_items.append(soln_item.strip())

        soln_item = f"""
        Simplify the left side.

        $\\displaystyle 
        \\implies
        {net_factor}
        {latex(y)} 
        = {num_4_latex} 
        $ 
        """
        soln_items.append(soln_item.strip())

        ###########################
        if n_base != 10:
            non_base_10_soln_items, base_10_value = self._non_base_10_value_soln(
                        n_base, num_as_integer, non_recu_as_integer, net_factor, y)
            soln_items.extend(non_base_10_soln_items)
        else:
            base_10_value = int(num_as_integer) - int(non_recu_as_integer)
            pass
        
        soln_item = f"""
        Ignore the base as all of expressions are on base 10.

        $\\displaystyle 
        \\implies
        {net_factor}
        {latex(y)} 
        = {base_10_value} 
        $ 
        """
        soln_items.append(soln_item.strip())

        num_base10_latex = latex_fraction(base_10_value, net_factor)

        soln_item = f"""
        $\\displaystyle 
        \\implies
        {latex(y)} 
        = {num_base10_latex} 
        $ 
        """
        soln_items.append(soln_item.strip())



        # recu_factor non_recu_factor decimal_factor
        
        soln_s = "\n\n".join(soln_items)
        
        result = {
            'soln_s': soln_s,
            'num_base10_latex': num_base10_latex,
            }
        result.update(random_num_data)
        return result

    def _non_base_10_value_soln(self, n_base, 
                                num_as_integer, non_recu_as_integer, 
                                net_factor, y):
        soln_items = []
        
        
        pos_data1 = self._get_pos_values_latex_of_int(num_as_integer, n_base)
        pos_data2 = self._get_pos_values_latex_of_int(non_recu_as_integer, n_base)
        base_10_value = pos_data1['base_10_value'] - pos_data2['base_10_value']

        base_10_value_latex = f"""
        \\left({{
        {pos_data1['base_10_value']} - {pos_data2['base_10_value']}
        }}\\right)_{{10}}
        """
        
        
        soln_item = f"""

        $\\displaystyle 
        
        \\begin{{aligned}}
        
        \\implies {net_factor} {latex(y)} 
        
        & =  {pos_data1['pos_values_latex']} \\\\ 
        & \\quad - {pos_data2['pos_values_latex']}  \\\\ \\\\

        & =  {pos_data1['base_10_latex']} \\\\ 
        & \\quad - {pos_data2['base_10_latex']}  \\\\ \\\\

        & = {pos_data1['base_10_value_latex']} - {pos_data2['base_10_value_latex']} \\\\ \\\\

        & = {base_10_value_latex}

        \\end{{aligned}}
        
        $ 
        """
        soln_items.append(soln_item.strip())
        return soln_items, base_10_value

    def _get_pos_values_latex_of_int(self, int_num, n_base):
        num_str = str(int_num)
        num_4_len = len(num_str)
        base_10_digit_list = [self.convert_digit_base_to_10(x, n_base) for x in num_str]
        pos_values_latex = [f"{x} \\cdot {n_base}^{{{num_4_len-i-1}}}" 
                      for i, x in enumerate(base_10_digit_list)]
        
        
        pos_values_latex = " + ".join(pos_values_latex)
        pos_values_latex = f"\\left({{{pos_values_latex}}}\\right)_{{10}}"

        base_10_parts = [int(x) * n_base ** (num_4_len - i - 1)
                      for i, x in enumerate(base_10_digit_list)]
        
        base_10_value = Add(*base_10_parts)
        base_10_value_latex = f"{base_10_value}"
        
        base_10_latex = [f"{x}" for x in base_10_parts]
        base_10_latex = " + ".join(base_10_latex)

        base_10_latex = f"\\left({{{base_10_latex}}}\\right)_{{10}}"
        base_10_value_latex = f"\\left({{{base_10_value_latex}}}\\right)_{{10}}"

        result = {
            'pos_values_latex': pos_values_latex,
            'base_10_latex': base_10_latex,
            'base_10_value_latex': base_10_value_latex,
            'base_10_value': base_10_value,
            }
        return result

    def get_n_digit_number_as_algebraic_expr(self, 
                                     n_base = 10, 
                                     n_digits = None,
                                     decimal_digits = 0,
                                     var_symbs = None):
        
        if n_digits is None:
            n_digits = random.randint(2, 6)
        
        if var_symbs is None:
            var_symbs = [Symbol(symb) for symb in 
                         list('zyxwvutsrqp')[: n_digits]]
        
        num_dict = []
        for i in range(n_digits):
            digit = var_symbs[i]
            num_dict.append([i, digit])
            pass
        
        num_dict.reverse()

        
        decimal_dict = []
        if decimal_digits:
            decimal_symbs = [Symbol(symb) for symb in 
                         list('abcdefgh')[: decimal_digits]]
            for i in range(decimal_digits):
                digit = decimal_symbs[i]
                decimal_dict.append([-i-1, digit])
                pass
            pass
        
        
        num = list_to_str([ symb for i, symb in num_dict], sep = "")
        if decimal_dict:
            decimal_part = list_to_str([x for i, x in decimal_dict], sep = "")
            num = f"{num}.{decimal_part}"
            
            num_dict.extend(decimal_dict)
            pass

        
        num_latex = f"\\left({num}\\right)_{{{n_base}}}"
        
        pos_values_latex = [f"{symb} \\; {n_base}^{{{i}}}" 
                      for i,  symb in num_dict]
        
        pos_values_latex_str = list_to_str(pos_values_latex, " + ")
        
        result = {
            'var_symbs': var_symbs,
            'num': num,
            'num_latex': num_latex,
            'n_base': n_base,
            'pos_values_latex': pos_values_latex,
            'pos_values_latex_str': pos_values_latex_str,
            }
        
        return result


    def derive_algebraic_expr(self, 
                             n_base = 10, 
                             n_digits = None,
                             var_symbs = None):
        
        if n_digits is None:
            n_digits = random.randint(2, 6)
        
        if var_symbs is None:
            var_symbs = [Symbol(symb) for symb in list('zyxwvu')[: n_digits]]
        else:
            var_symbs.reverse()
        
        num_dict = []
        for i, digit in enumerate(var_symbs):
            num_dict.append([i, digit])
            pass
        
        num_dict.reverse()
        
        num = list_to_str([ symb for i, symb in num_dict], sep = "")
        
        num_latex = f"\\left({num}\\right)_{{{n_base}}}"
        
        pos_values_latex = [f"\\left({symb}\\right) * {n_base}^{{{i}}}" 
                      for i,  symb in num_dict]
        
        pos_values_latex_str = list_to_str(pos_values_latex, " + ")
        
        result = {
            'var_symbs': var_symbs,
            'num': num,
            'num_latex': num_latex,
            'n_base': n_base,
            'pos_values_latex': pos_values_latex,
            'pos_values_latex_str': pos_values_latex_str,
            }
        
        return result



    pass
#class PositionalValueHelper:
