'''
Created on 12-Jul-2021

@author: bthakur
'''
import random
import inspect
import traceback   #        {inspect.stack()[0][3]}

from sympy import (Eq, Symbol, symbols, sqrt, latex,
                   Rational, solve, log, nsimplify, E, exp,  
                   sin, cos, I, series, pi,
                   UnevaluatedExpr as ueval, simplify,
                   Add, Mul
                   )
from collections import OrderedDict
import sympy

from xv.km.util import latex_fraction, latex_exponent
from xv.util import toIntOrFloat, list_to_str, convert_to_symb


# _statement_to_expr_helper
# StatementToExprHelper

from xv.util import UtilClass

class StatementToExprHelper(UtilClass):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        
        pass
    
    def get_problem_arithmetic_opreration(self, *args, **kwargs):
        from .stmt_to_expr import StmtToExprArithmeticOpreration
        soe_ao = StmtToExprArithmeticOpreration(verbose = self.verbose)
        result = soe_ao.get_problem_arithmetic_opreration(*args, **kwargs)
        return result
        pass

    def get_problem_assorted(self, *args, **kwargs):
        from .modelling import ModellingAssortedHelper
        model = ModellingAssortedHelper(verbose = self.verbose)
        result = model.get_random_model(*args, **kwargs)
        return result
        pass

    def get_problem_single_variable(self, *args, **kwargs):
        from .modelling import ModellingSingleVariableHelper
        model = ModellingSingleVariableHelper(verbose = self.verbose)
        result = model.get_random_model(*args, **kwargs)
        return result
        pass

    def get_problem_modelling_two_variable(self, *args, **kwargs):
        from .modelling import ModellingTwoVariableHelper
        model = ModellingTwoVariableHelper(verbose = self.verbose)
        result = model.get_random_model(*args, **kwargs)
        return result
        pass

    def get_problem_modelling_two_digits(self, *args, **kwargs):
        from .modelling import ModellingTwoDigitsHelper
        model = ModellingTwoDigitsHelper(verbose = self.verbose)
        result = model.get_random_model(*args, **kwargs)
        return result
        pass

    def get_problem_modelling_simple_percentage(self, *args, **kwargs):
        from .modelling import ModellingPercentageHelper
        model = ModellingPercentageHelper(verbose = self.verbose)
        result = model.get_random_simple_model(*args, **kwargs)
        return result
        pass



    def get_problem_modelling_percentage(self, *args, **kwargs):
        from .modelling import ModellingPercentageHelper
        model = ModellingPercentageHelper(verbose = self.verbose)
        result = model.get_random_model(*args, **kwargs)
        return result
        pass


    def get_problem_modelling_multi_variable(self, *args, **kwargs):
        from .modelling import ModellingMultiVariableHelper
        model = ModellingMultiVariableHelper(verbose = self.verbose)
        result = model.get_random_model(*args, **kwargs)
        return result
        pass

    def get_problem_modelling_juice_business(self, *args, **kwargs):
        from .modelling import ModellingJuiceBusinessHelper
        model = ModellingJuiceBusinessHelper(verbose = self.verbose)
        result = model.get_random_model(*args, **kwargs)
        return result
        pass


    def get_problem_modelling_layered_business(self, *args, **kwargs):
        from .modelling import ModellingLayeredBusinessHelper
        model = ModellingLayeredBusinessHelper(verbose = self.verbose)
        result = model.get_random_model(*args, **kwargs)
        return result
        pass



    def get_discounts_taxes_prob(self):
        
        names = [['Shelly', 'She'],
                 ['John', 'He'],
                 ]
        shopping_items = ["shirt", "watch", "phone"]
        currencies = [Symbol('\$')]
        pct = Symbol("\%")
        pct_val = Rational(1, 100)
        
        name, pronoun = random.choice(names)
        shopping_item = random.choice(shopping_items)
        currency = random.choice(currencies)
        currency_ltx = latex(currency)
        
        
        problem_items = []
        soln_items = []
        
        retail_price = random.randint(5, 9) * 100

        problem_item = f"""
        {name} goes for shopping. {pronoun} buys a {shopping_item}. 
        The retail price of the {shopping_item} is $
        \\displaystyle {currency_ltx}{latex(retail_price)}$. 
        """
        problem_items.append(problem_item.replace("\n", " ").strip())

        soln_item = f"""
        Retail Price $
        \\displaystyle = {currency_ltx}{latex(retail_price)} $
        """
        soln_items.append(soln_item.strip())

        
        ###########################################
        disc0_pct = random.randint(1, 5) * 10 * pct
        disc0_price_ueval = retail_price * ueval(ueval(100 * pct) - ueval(disc0_pct)) 
        disc0_price = retail_price * ueval(100 * pct - disc0_pct) 

        disc0_price_cal = retail_price * (100 * pct - disc0_pct) 
        disc0_price_cal = simplify(disc0_price_cal.subs({pct: pct_val}))
        disc0_price_cal = toIntOrFloat(float(disc0_price_cal), 2)
        
        
        problem_item = f"""
        The shop offers a discount of $\\displaystyle {latex(disc0_pct)}$.
        """
        problem_items.append(problem_item.replace("\n", " ").strip())
        
        soln_item = f"""
        Shop discount $\\displaystyle = {latex(disc0_pct)}$
  
        $$\\displaystyle
        \\begin{{align}}

        \\text{{Price after shop discount}} &
        = {currency_ltx}{latex(disc0_price_ueval)}
        \\\\
        
        &
        = {currency_ltx}{latex(disc0_price)}
        \\\\
        
        &
        = {currency_ltx}{latex(disc0_price_cal)}
        \\\\
        
        \\end{{align}}
        $$
        
        """
        soln_items.append(soln_item.strip())


        ##################################
        disc1_pct = random.randint(1, 3) * 10 * pct
        disc1_price_ueval = ueval(disc0_price_cal) * ueval(ueval(100 * pct) - ueval(disc1_pct)) 
        disc1_price = ueval(disc0_price_cal) * ueval(100 * pct - disc1_pct) 

        disc1_price_cal = disc0_price_cal * (100 * pct - disc1_pct) 
        disc1_price_cal = simplify(disc1_price_cal.subs({pct: pct_val}))
        disc1_price_cal = toIntOrFloat(float(disc1_price_cal), 2)
        
        problem_item = f"""
        There is an additional festival discount of 
        $\\displaystyle {latex(disc1_pct)}$ on the discounted price.
        """
        problem_items.append(problem_item.replace("\n", " ").strip())
        
        soln_item = f"""
        Festival discount $\\displaystyle = {latex(disc1_pct)}$
  
        $$\\displaystyle
        \\begin{{align}}

        \\text{{Price after festival discount}} &
        = {currency_ltx}{latex(disc1_price_ueval)}
        \\\\
        
        &
        = {currency_ltx}{latex(disc1_price)}
        \\\\
        
        &
        = {currency_ltx}{latex(disc1_price_cal)}
        \\\\
        
        \\end{{align}}
        $$
        
        """
        soln_items.append(soln_item.strip())



        ##################################
        disc2_coupon = random.randint(1, 3) * 10
        disc2_price = ueval(disc1_price_cal) - ueval(disc2_coupon)

        disc2_price_cal = disc1_price_cal - disc2_coupon
        disc2_price_cal = toIntOrFloat(float(disc2_price_cal), 2)
        
        problem_item = f"""
        {pronoun} also has a loyalty coupon of  
        $\\displaystyle {currency_ltx}{latex(disc2_coupon)}$.
        """
        problem_items.append(problem_item.replace("\n", " ").strip())
        
        soln_item = f"""
        Loyalty coupon $\\displaystyle 
        = {currency_ltx}{latex(disc2_coupon)}
        $
  
        $$\\displaystyle
        \\begin{{align}}

        \\text{{Price after loyalty coupon}} &
        = {currency_ltx}{latex(disc1_price_cal)} - {currency_ltx}{latex(disc2_coupon)}
        \\\\
        
        &
        = {currency_ltx}{latex(disc2_price_cal)}
        \\\\
        
        \\end{{align}}
        $$
        
        """
        soln_items.append(soln_item.strip())

        ##################################
        tax0_pct = random.randint(10, 20) * pct
        tax0_price_ueval = ueval(disc2_price_cal) * ueval(ueval(100 * pct) + ueval(tax0_pct)) 
        tax0_price = ueval(disc2_price_cal) * ueval(100 * pct + tax0_pct) 

        tax0_price_cal = disc2_price_cal * (100 * pct + tax0_pct) 
        tax0_price_cal = simplify(tax0_price_cal.subs({pct: pct_val}))
        tax0_price_cal = toIntOrFloat(float(tax0_price_cal), 2)
        
        problem_item = f"""
        A local tax is levied $\\displaystyle @{latex(tax0_pct)}$.
        """
        problem_items.append(problem_item.replace("\n", " ").strip())
        
        soln_item = f"""
        Local tax $\\displaystyle = {latex(tax0_pct)}$
  
        $$\\displaystyle
        \\begin{{align}}

        \\text{{Price after local tax}} &
        = {currency_ltx}{latex(tax0_price_ueval)}
        \\\\
        
        &
        = {currency_ltx}{latex(tax0_price)}
        \\\\
        
        &
        = {currency_ltx}{latex(tax0_price_cal)}
        \\\\
        
        \\end{{align}}
        $$
        
        """
        soln_items.append(soln_item.strip())


        ##################################
        cashback_coupon = random.choice([25, 50, 75, 100])
        
        cashback_pct = random.choice([5, 10, 15, 20]) * pct
        cashback_pct_cal_ueval = ueval(tax0_price_cal) * ueval(cashback_pct)
        
        cashback_pct_cal = tax0_price_cal * cashback_pct
        cashback_pct_cal = simplify(cashback_pct_cal.subs({pct: pct_val}))
        cashback_pct_cal = toIntOrFloat(float(cashback_pct_cal), 2)
        
        cashback = min([cashback_coupon, cashback_pct_cal])
        
        cashback_price = tax0_price_cal - cashback 
        
        problem_item = f"""
        Her credit card offers her an instant cashback of 
        $\\displaystyle {currency_ltx}{latex(cashback_coupon)}$.
        subject to $\\displaystyle {latex(cashback_pct)}$ of the final price.
        """
        problem_items.append(problem_item.replace("\n", " ").strip())
        
        soln_item = f"""
        
        Cashback is $\\displaystyle 
        {currency_ltx}{latex(cashback_coupon)}
        $ but cannot exceed $
        \\displaystyle {latex(cashback_pct)}
        $ of of the final price $
        \\displaystyle {currency_ltx}{latex(tax0_price_cal)}
        $.
        
        $$\\displaystyle
        \\begin{{align}}

        \\text{{Cashback}} &
        = \\text{{min}} \\left(
        {currency_ltx}{latex(cashback_coupon)}, \\;
        {currency_ltx}{latex(cashback_pct_cal_ueval)}
        \\right)
        \\\\
        
        &
        = \\text{{min}} \\left(
        {currency_ltx}{latex(cashback_coupon)}, \\;
        {currency_ltx}{latex(cashback_pct_cal)}
        \\right)
        \\\\
        
        &
        = {currency_ltx}{latex(cashback)}
        \\\\
        
        \\end{{align}}
        $$
        
        $$\\displaystyle
        \\begin{{align}}

        \\text{{Price after cashback}} &
        = {currency_ltx}{latex(tax0_price_cal)} - {currency_ltx}{latex(cashback)}
        \\\\
        
        &
        = {currency_ltx}{latex(cashback_price)}
        \\\\
        
        \\end{{align}}
        $$
        
        
        """
        soln_items.append(soln_item.strip())


        problem_item = f"""
        So, how much {name} has to pay for the {shopping_item}?
        """
        problem_items.append(problem_item.replace("\n", " ").strip())
        
        soln_item = f"""
        Therefore, {name} will pay $
        \\displaystyle {currency_ltx}{latex(cashback_price)}
        $ for the {shopping_item}.
        """
        soln_items.append(soln_item.strip())


        answer_s = f"""
        $
        \\displaystyle {currency_ltx}{latex(cashback_price)}
        $
        """
        
        problem_s = " ".join(problem_items)
        soln_s = "\n\n".join(soln_items)
        
        
        
        model = {
            'problem_s': problem_s.strip(),
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        return model
        pass
    
    def get_mul_digit_num_prob(self):
        
        problem_items = []
        soln_items = []


        
        #################################################
        ## Part 0
        num = random.choice([random.randint(10, 99),
                             random.randint(100, 999),
                             random.randint(1000, 9999)])
        #num_str = f"{num}"
        num_len = len(f"{num}")
        computed_num = num
        
        if num_len == 2:
            digits_symbs = symbols("x y")
        else:
            digits_symbs = symbols("u v w x y z")[-num_len:]
       
        pos_keys = [10**(num_len - idx - 1) for idx in range(num_len)]
        dgts = [int(item) for item in list(f"{computed_num}")]
        dgt_exprs = list(digits_symbs).copy()
        dgt_expr_symbs = dgt_exprs.copy()
        
        #k = pos_key (int)
        #v = {dgt, dgt_expr, dgt_expr_symb}
        num_dict = {k: (dgts[idx], dgt_exprs[idx], dgt_expr_symbs[idx])
                    for idx, k in enumerate(pos_keys)}
        
        num_dict = OrderedDict(num_dict)
        
        dgt_expr_symbs_str = [f"{latex(k)} \\cdot {latex(v[2])}"
                           for k,v in num_dict.items()]
        
        dgt_expr_symbs_str = Symbol(" + ".join(dgt_expr_symbs_str))
        
        problem_item = f"""
        We have {num_len}-digit number.
        """
        problem_items.append(problem_item.replace("\n", " ").strip())

        if self.verbose:
            soln_item = f"""
            ## num = {num}
            """
            soln_items.append(soln_item.strip())
            

        soln_item = f"""
        We can write a {num_len}-digit number as:
        
        $\\displaystyle {latex(dgt_expr_symbs_str)}$ 
        
        """
        soln_items.append(soln_item.strip())

        ################################################
        ## add steps problems
        
        steps = [self._mul_digit_num_prob_step_add_a_number,
                 self._mul_digit_num_prob_step_reverse_digits,
                 self._mul_digit_num_prob_step_interchange_digits,
                 self._mul_digit_num_prob_step_double_or_halve_a_digit
                 ]
        
        random.shuffle(steps)
        
        for step in steps:
            item_result = step(num_dict)
            if item_result is None:
                continue
            
            (num_dict, step_problem_items, step_soln_items) = item_result

            problem_items.extend(step_problem_items)
            soln_items.extend(step_soln_items)

            pass



        #####################
        ## Solve for digits
        #v = {dgt, dgt_expr, dgt_expr_symb}
        computed_num = Add(*[k * v[0] for k, v in num_dict.items()])
        computed_expr = Add(*[k * v[1] for k, v in num_dict.items()])
        
        num_terms_symbs = [f"{latex(convert_to_symb(k * v[1]))}"
                             for k,v in num_dict.items()]
        
        num_terms_symbs = Symbol(" + ".join(num_terms_symbs))

        terms_soln_eq = Eq(computed_expr, computed_num)
        
        soln_item = f"""
        $\\displaystyle = {latex(num_terms_symbs)}$ 
        
        $\\displaystyle = {latex(computed_expr)}$ 

        Therefore, according to question, 
        
        $\\displaystyle {latex(terms_soln_eq)}$ 
        
        """
        soln_items.append(soln_item.strip())

 
        lhs = computed_expr
        rhs = computed_num
        if isinstance(computed_expr, Add):
            lhs_items = []
            for item in computed_expr.args:
                if hasattr(item, 'free_symbols'):
                    if item.free_symbols:
                        lhs_items.append(item)
                        continue
                    pass
                rhs = rhs - item
                pass
            lhs = Add(*lhs_items)
            pass
        
        terms_soln_eq = Eq(lhs, rhs)
        
        soln_item = f"""
        $\\displaystyle \\implies {latex(terms_soln_eq)}$ 
        """
        soln_items.append(soln_item.strip())

 
        
        #####################


        answer_s = f"""
        $\\displaystyle {latex(num)}$ 
        """
        
        problem_s = " ".join(problem_items)
        soln_s = "\n\n".join(soln_items)
        
        
        model = {
            'problem_s': problem_s.strip(),
            'soln_s': soln_s,
            'answer_s': answer_s,
            }
        return model
        pass
    

    def _mul_digit_num_prob_step_add_a_number(self, num_dict):
        #################################################
        ## Part 1

        #v = {dgt, dgt_expr, dgt_expr_symb}
        computed_num = Add(*[k * v[0] for k, v in num_dict.items()])

        ## add a number
        num_str = f"{computed_num}"
        
        print(f"""
        num_dict = {repr(num_dict)}
        num_str = {num_str}
        """)

        addl_num = 0
        while addl_num == 0:        
            addl_num_str = [f"{random.randint(0, 9 - int(item))}"  for item in num_str]
            addl_num_str = "".join(addl_num_str)
            addl_num = int(addl_num_str)
        
        keys = list(num_dict.keys())
        mod_num_dict = OrderedDict({})
        for idx, k in enumerate(keys):
            #v = {dgt, dgt_expr, dgt_expr_symb}
            dgt = num_dict[k][0] + int(addl_num_str[idx])
            dgt_expr = num_dict[k][1] + int(addl_num_str[idx])
            dgt_expr_symb = convert_to_symb(dgt_expr)
            mod_num_dict[k] = (dgt, dgt_expr, dgt_expr_symb)
            pass
        
        #v = {dgt, dgt_expr, dgt_expr_symb}
        computed_num = Add(*[k * v[0] for k, v in mod_num_dict.items()])
        
        num_terms_symbs = [f"{latex(k)} \\cdot {latex(v[2])}"
                             for k,v in mod_num_dict.items()]
        
        num_terms_symbs = Symbol(" + ".join(num_terms_symbs))

        problem_items = []
        soln_items = []
        
        problem_item = f"""
        Add $\\displaystyle {latex(addl_num)}$ the number.
        """
        problem_items.append(problem_item.replace("\n", " ").strip())

        soln_item = f"""
        After adding $\\displaystyle {latex(addl_num)}$ to the number:
        
        $\\displaystyle {latex(num_terms_symbs)}$  
        """
        
        soln_items.append(soln_item.strip())
        
        if self.verbose:
            soln_item = f"""
            ## computed_num = {computed_num}
            """
            soln_items.append(soln_item.strip())
            
            
        
        return mod_num_dict, problem_items, soln_items
         
    def _mul_digit_num_prob_step_reverse_digits(self, num_dict):
        #################################################
        ## Part 2

        ## reverse the digits

        keys = list(num_dict.keys())
        rev_keys = keys[::-1]
        mod_num_dict = OrderedDict({})
        for idx, k in enumerate(keys):
            rev_k = rev_keys[idx]
            mod_num_dict[k] = num_dict[rev_k] 
            pass

        #v = {dgt, dgt_expr, dgt_expr_symb}
        computed_num = Add(*[k * v[0] for k, v in mod_num_dict.items()])
        
        num_terms_symbs = [f"{latex(k)} \\cdot {latex(v[2])}"
                             for k,v in mod_num_dict.items()]
        
        num_terms_symbs = Symbol(" + ".join(num_terms_symbs))

        problem_items = []
        soln_items = []
        
        problem_item = f"""
        Reverse the number now.
        """
        problem_items.append(problem_item.replace("\n", " ").strip())

        soln_item = f"""
        Reversing the number:
        
        $\\displaystyle {latex(num_terms_symbs)}$ 
        """
        soln_items.append(soln_item.strip())

        if self.verbose:
            soln_item = f"""
            ## computed_num = {computed_num}
            """
            soln_items.append(soln_item.strip())
            
        return mod_num_dict, problem_items, soln_items
         
    def _mul_digit_num_prob_step_interchange_digits(self, num_dict):
        
        #################################################
        ## Part 3
        
        ## interchange digits
        keys = list(num_dict.keys())
        key0, key1 = random.sample(keys, 2)

        mod_num_dict = num_dict.copy()
        mod_num_dict[key0] = num_dict[key1] 
        mod_num_dict[key1] = num_dict[key0] 
        
        #v = {dgt, dgt_expr, dgt_expr_symb}
        computed_num = Add(*[k * v[0] for k, v in mod_num_dict.items()])

        num_terms_symbs = [f"{latex(k)} \\cdot {latex(v[2])}"
                             for k,v in mod_num_dict.items()]
        
        num_terms_symbs = Symbol(" + ".join(num_terms_symbs))


        problem_items = []
        soln_items = []

        
        problem_item = f"""
        Interchange the {key0}th and {key1}th positions.
        """
        problem_items.append(problem_item.replace("\n", " ").strip())

        soln_item = f"""
        After interchanging the {key0}th and {key1}th positions:
        
        $\\displaystyle {latex(num_terms_symbs)}$ 
        
        """
        soln_items.append(soln_item.strip())

        if self.verbose:
            soln_item = f"""
            ## computed_num = {computed_num}
            """
            soln_items.append(soln_item.strip())
        
        return mod_num_dict, problem_items, soln_items
    
    def _mul_digit_num_prob_step_double_or_halve_a_digit(self, num_dict):
        
        #################################################
        ## Part 4
        
        ## double or halve a digit
        keys = list(num_dict.keys())
        mod_num_dict = num_dict.copy()

        duble_halve_stmt = None
        for idx, k in enumerate(keys):
            v = mod_num_dict[k]
            if v[0] < 5:
                #v = {dgt, dgt_expr, dgt_expr_symb}
                duble_halve_stmt = f"Double the {k}th position digit."
                dgt = v[0] * 2
                dgt_expr = v[1] * 2
                dgt_expr_symb = ueval(v[2]) * 2
                mod_num_dict[k] = (dgt, dgt_expr, dgt_expr_symb)
                break 

            if v[0] % 2 == 0:
                duble_halve_stmt = f"Halve the {k}th position digit."
                dgt = v[0] // 2
                dgt_expr = v[1] / 2
                dgt_expr_symb = ueval(v[2]) / 2
                mod_num_dict[k] = (dgt, dgt_expr, dgt_expr_symb)
                break 
            pass
        
        if duble_halve_stmt is None:
            return None
        
        #v = {dgt, dgt_expr, dgt_expr_symb}
        computed_num = Add(*[k * v[0] for k, v in mod_num_dict.items()])


        num_terms_symbs = [f"{latex(k)} \\cdot {latex(v[2])}"
                             for k,v in mod_num_dict.items()]
        
        num_terms_symbs = Symbol(" + ".join(num_terms_symbs))


        problem_items = []
        soln_items = []

        
        problem_item = f"""
        {duble_halve_stmt}
        """
        problem_items.append(problem_item.replace("\n", " ").strip())

        soln_item = f"""
        {duble_halve_stmt}
        
        $\\displaystyle {latex(num_terms_symbs)}$ 
        
        """
        soln_items.append(soln_item.strip())

        if self.verbose:
            soln_item = f"""
            ## computed_num = {computed_num}
            """
            soln_items.append(soln_item.strip())
        
        return mod_num_dict, problem_items, soln_items
    
         
         
    
    pass
#class StatementToExprHelper:
