'''
Created on 20-Jan-2022

@author: bthakur
'''

import inspect
import traceback
import random
import numpy as np
from sympy import (latex, Symbol, symbols,
                   sympify, simplify
                   
                   )


from xv.km.util import latex_fraction
from sympy.core.expr import UnevaluatedExpr as ueval

from xv.km.ui import SvgHelper
from xv.util.data import CacheUtil

import matplotlib.pyplot as plt
from xv.plotter.core import Plotter2D, Plotter3D

from xv.util import toIntOrFloat, list_to_str
import random

from .._arithmetic import ArithmeticExpressionHelper

# _crypto_helper
# CryptoHelper

from xv.util import UtilClass

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


    def get_random_crypto_problem(self, keyword = None, one_to_one = True):
        ops = ['+', '-', '*', '/']
        try:
            ops_dict = self.shuffle_ops(ops, one_to_one = one_to_one)
            ops_dict_latex, rev_ops_dict_latex = self._convert_op_dict_to_latex(ops_dict)
            expr_pair_data =  self.get_arithmetic_expr_pair_data(ops_dict)
            
            real_expr = expr_pair_data['real_expr']
            real_expr_eval = expr_pair_data['real_expr_eval']
            mod_expr_str = expr_pair_data['mod_expr_str']
            real_expr_str = expr_pair_data['real_expr_str']
    
            if len(ops_dict.keys()) == len(set(ops_dict.values())):
                ae_helper = ArithmeticExpressionHelper(verbose = self.verbose)
                soln_result = ae_helper.get_solution(real_expr)
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}: {e}
            ops = {ops}
            keyword = {keyword}
            one_to_one = {one_to_one}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            return None
        
        
        problem_s = f"""
        We have replaced the operators as: 
        $\\displaystyle {ops_dict_latex}$
        What is real value of the expression $\\displaystyle {mod_expr_str}$
        """

        soln_items = []
        
        if len(ops_dict.keys()) != len(set(ops_dict.values())):
            soln_item = f"""
            As the mapping is not one-to-one (or injective ), 
            i.e., two operators have been changed to a single operator, 
            we cannot do a reverse operation.
            <br/>
            Hence, we cannot find the original expression and 
            it cannot be determined as information is lost.
            """
            soln_items.append(soln_item.strip().replace("\n", ""))
            
            answer_s = f"""
            It cannot be determined as information is lost.
            """
            pass
        else:
            
            soln_item = f"""
            Modified expression $\\displaystyle = {mod_expr_str}$
            
            The reverse operation of operators will be:
            $\\displaystyle {rev_ops_dict_latex}$
            Using reverse replacement of operators, we rewrite the expression:
            
            $\\displaystyle = {real_expr_str}$
            
            = {soln_result['soln_s'].strip()}
            
            $\\displaystyle = {latex(real_expr_eval)}$
            """
            soln_items.append(soln_item.strip())
            
            
            answer_s = f"""
            $\\displaystyle {latex(real_expr_eval)}$
            """
            pass
    
        
        soln_s = "\n\n".join(soln_items)

        result = {
            'problem_s': problem_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }
        
        result.update(expr_pair_data)
        
        return result
        pass

    def get_arithmetic_expr_pair_data(self, ops_dict, first_ops = ['+', '-']):
        real_expr, mod_expr = self._get_arithmetic_expr_pair_strs(
                                            ops_dict, first_ops)
        real_expr_str = " ".join(real_expr)
        mod_expr_str = " ".join(mod_expr)

        try:
            real_expr = sympify(real_expr_str, evaluate = False)
            real_expr_eval = simplify(real_expr)
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}: {e}
            ops_dict = {ops_dict}
            real_expr = {real_expr}
            real_expr_str = {real_expr_str}
            mod_expr = {mod_expr}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            return None
        
        result = {
            'ops_dict': ops_dict,
            'real_expr': real_expr,
            'real_expr_str': real_expr_str,
            'real_expr_eval': real_expr_eval,
            'mod_expr_str': mod_expr_str,
            }
        return result
        pass
    
    def shuffle_ops(self, ops, one_to_one = True):
        try:
            if not one_to_one:
                ops_dict = [(x, random.choice(ops)) for x in ops]
                ops_dict = dict(ops_dict)
                return ops_dict
                pass
            
            ops_dict = {}
            ops_copy = ops.copy()
    
            for op in ops:
                temp_ops = list(set(ops_copy).difference(ops_dict.values()))
                if len(temp_ops) > 1:
                    temp_ops = [x for x in temp_ops if x != op]
                
                sOut = f"""
                op = {op}
                temp_ops = {temp_ops}
                ops_dict = {ops_dict}
                """
                #self._log(sOut)
                
                ops_dict[op] = random.choice(temp_ops)
                pass
            return ops_dict
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}: {e}
            ops = {ops}
            one_to_one = {one_to_one}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            return None
        pass

    def _get_arithmetic_expr_pair_strs(self, ops_dict, first_ops):
        ops = list(ops_dict.keys())
        
        real_expr = []
        mod_expr = []
        while (len(real_expr) < 3 or 
               random.choice([True, False])):
            if len(real_expr) == 0:
                op = random.choice([x for x in ops if x in first_ops])
            else:
                op = random.choice(ops)
                pass
            
            mod_op = ops_dict[op]
            num = random.randint(0, 10)
            
            real_expr.append([op, num])
            mod_expr.append([mod_op, num])
            pass

        real_expr = [f"{op} {num}" for op, num in real_expr]
        mod_expr = [f"{op} {num}" for op, num in mod_expr]
        return real_expr, mod_expr
    
    
    
    
    def _convert_op_dict_to_latex(self, ops_dict):
        ops_dict_latex = [f"{k} \\longrightarrow {v}" for k,v in ops_dict.items()]
        ops_dict_latex = list_to_str(ops_dict_latex, sep = " \\\\")

        rev_ops_dict_latex = [f"{v} \\longrightarrow {k}" for k,v in ops_dict.items()]
        rev_ops_dict_latex = list_to_str(rev_ops_dict_latex, sep = " \\\\" )
        return ops_dict_latex, rev_ops_dict_latex
        
    def _convert_dict_to_strs(self, ops_dict):
        ops_dict_str = [f"{k} with {v}" for k,v in ops_dict.items()]
        ops_dict_str = list_to_str(ops_dict_str, last_sep = " and ")

        rev_ops_dict_str = [f"{v} with {k}" for k,v in ops_dict.items()]
        rev_ops_dict_str = list_to_str(rev_ops_dict_str, last_sep = " and ")
        return ops_dict_str, rev_ops_dict_str
        
    
    def _log_DELETED(self, sOut, to_print = None):
        if to_print is None:
            to_print = self.verbose
            
        if to_print:
            print(sOut)
    

    pass
#class CryptoHelper:
