'''
Created on 10-Jun-2021

@author: bthakur
'''
from xv.util import toIntOrFloat
from sympy import (symbols, Rational, sympify, pi, E, I,
                   latex, solve, Eq, simplify,
                   sqrt,
                   UnevaluatedExpr as ueval
                   )
import sympy

import random
from xv.km.util import latex_fraction
import operator
import inspect
import traceback

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

    def get_random_ns_prop(self,
                            ns_name = None,
                            prop_name = None,
                            op = None,
                            is_advanced = False
                            ):
        if ns_name is None:
            ns_name = random.choice(self.get_ns_names(is_advanced))

        if prop_name is None:
            prop_name = random.choice(self.get_prop_names())

        if op is None:
            while True:
                op = random.choice(self.get_ops(ns_name = ns_name,
                                                prop_name = prop_name,
                                                details = False))
                if not (ns_name == 'complex' and op == operator.truediv):
                    break
                    pass

        op = self._op_to_str(op)

        is_true = self.get_ns_props(ns_name, prop_name, op)

        op_details = self.get_op_details(op)

        result = {
            "ns_name": ns_name,
            "prop_name": prop_name,
            "op": op,
            "op_details": op_details,
            "is_true": is_true,
            }

        example = self.get_ns_prop_example(
                            ns_name, prop_name, op, is_true)

        result.update(example)
        return result
        pass

    def get_ns_prop_example(self,
                           ns_name,
                           prop_name,
                           op,
                           is_true):
        a, b, c = symbols('a, b, c')

        op_details = self.get_op_details(op)

        try:
            number_method_name = f"get_{ns_name}_numbers"
            if not hasattr(self, number_method_name):
                raise Exception(f"Invalid ns_name: {ns_name}")

            statement_method_name = f"get_{prop_name}_statement"
            if not hasattr(self, statement_method_name):
                raise Exception(f"Invalid method: {statement_method_name}")

            number_method = getattr(self, number_method_name)
            statement_method = getattr(self, statement_method_name)

            statement = None
            loop_count = 0
            while statement is None:
                loop_count = loop_count + 1
                n1, n2, n3 = number_method(3, simple = True)
                if n1 is None or n2 is None or n3 is None:
                    continue
                if n1 == sympy.nan or n2 == sympy.nan or n3 == sympy.nan:
                    continue

                try:
                    sOut = f"""
                    calling {statement_method_name} : {is_true}
                    """

                    statement = statement_method((a, b, c), (n1, n2, n3),
                                         ns_name, op_details, is_true)
                except Exception as e:
                    sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:
                    {e}"""
                    print(sError)
                    print(traceback.format_exc())
                    continue

                if loop_count > 20:
                    raise Exception(f"""
                    Failed method: {statement_method_name}
                    is_true: {is_true}
                    ns_name = {ns_name}
                    prop_name = {prop_name}
                    op = {op}
                    n1, n2, n3 = {n1}, {n2}, {n3}
                    """)

            result = {'n1': n1, 'n2': n2, 'n3': n3,
                      'statement': statement.strip()}
            return result

        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:
            {e}"""
            print(sError)
            print(traceback.format_exc())
            return {'is_true': False}
        pass

    def get_closure_statement(self, syms, nums, ns_name, op_details, is_true):
        a, b, c = syms
        n1, n2, n3 = nums

        n1_latex = self.get_var_latex(n1)
        n2_latex = self.get_var_latex(n2)
        n3_latex = self.get_var_latex(n3)

        op_sym = op_details[2]
        op = op_details[0]

        if n2 == 0 and op == operator.truediv:
            n1n2 = None
            test_result = False
        else:
            test_method_name = f"is_{ns_name}"

            if not hasattr(self, test_method_name):
                raise Exception(f"Invalid test_method_name: {test_method_name}")

            test_method = getattr(self, test_method_name)

            n1n2 = op(n1, n2)
            test_result = test_method(n1n2)

        if test_result != is_true:
            return None

        if is_true:
            is_true_stmt = "is also"
        else:
            is_true_stmt = f"is not"


        statement = f"""
        if $\\displaystyle {latex(a)}$ and $\\displaystyle {latex(b)}$ are {ns_name} numbers,
        $\\displaystyle {latex(a)} {op_sym} {latex(b)}$ should also be a {ns_name} number.

        Example:
        $\\displaystyle {latex(n1)}$ and $\\displaystyle {latex(n2)}$ are {ns_name} numbers.
        $\\displaystyle {n1_latex} {op_sym} {n2_latex} = {latex(n1n2)}$ {is_true_stmt} a {ns_name} number.
        """
        return statement
        pass

    def get_associative_statement(self, syms, nums, ns_name, op_details, is_true):
        sOut = f"""
        associative:
        ns_name = {ns_name}
        nums = {nums}
        is_true == {is_true}
        op_details = {op_details}
        """
        #print(sOut)

        a, b, c = syms
        n1, n2, n3 = nums

        n1_latex = self.get_var_latex(n1)
        n2_latex = self.get_var_latex(n2)
        n3_latex = self.get_var_latex(n3)

        op_sym = op_details[2]
        op = op_details[0]

        n1_n2n3 = simplify(op(n1, op(n2, n3)))
        n1n2_n3 = simplify(op(op(n1, n2), n3))


        if self._are_equal(n1_n2n3, n1n2_n3) != is_true:
            sOut = f"""
            {self._are_equal(n1_n2n3, n1n2_n3)} == {is_true}

            {n1}, {n2}, {n3}

            {n1_n2n3}

            {n1n2_n3}
            """
            print(sOut)
            return None

        if is_true:
            is_true_stmt = "="
        else:
            is_true_stmt = f"\\neq"

        statement = f"""
        if $\\displaystyle {latex(a)}$, $\\displaystyle {latex(b)}$ and $\\displaystyle {latex(c)}$ are {ns_name} numbers, then
        $\\displaystyle {latex(a)} {op_sym} ({latex(b)} {op_sym} {latex(c)} )
        = ({latex(a)} {op_sym} {latex(b)}) {op_sym} {latex(c)} $

        Example:
        $\\displaystyle {latex(n1)}$, $\\displaystyle {latex(n2)}$ and $\\displaystyle {latex(n3)}$ are {ns_name} numbers.

        $\\displaystyle {n1_latex} {op_sym} \\left({n2_latex} {op_sym} {n3_latex}\\right)
        = {latex(n1_n2n3)}$

        $\\displaystyle \\left({n1_latex} {op_sym} {n2_latex}\\right) {op_sym} {n3_latex}
        = {latex(n1n2_n3)}$

        $\\implies
        {n1_latex} {op_sym} \\left({n2_latex} {op_sym} {n3_latex}\\right)
        {is_true_stmt}
        \\left({n1_latex} {op_sym} {n2_latex}\\right) {op_sym} {n3_latex}$

        """
        return statement
        pass

    def get_commutative_statement(self, syms, nums, ns_name, op_details, is_true):
        sOut = f"""
        commutative:
        ns_name = {ns_name}
        nums = {nums}
        is_true == {is_true}
        op_details = {op_details}
        """
        #print(sOut)

        a, b, c = syms
        n1, n2, n3 = nums

        n1_latex = self.get_var_latex(n1)
        n2_latex = self.get_var_latex(n2)
        n3_latex = self.get_var_latex(n3)

        op_sym = op_details[2]
        op = op_details[0]

        n1n2 = simplify(op(n1, n2))
        n2n1 = simplify(op(n2, n1))

        if self._are_equal(n1n2, n2n1) != is_true:
            return None

        if is_true:
            is_true_stmt = "="
        else:
            is_true_stmt = f"\\neq"


        statement = f"""
        if $\\displaystyle {latex(a)}$ and $\\displaystyle {latex(b)}$ are {ns_name} numbers, then
        $\\displaystyle {latex(a)} {op_sym} {latex(b)} = {latex(b)} {op_sym} {latex(a)}$


        Example:
        $\\displaystyle {latex(n1)}$ and $\\displaystyle {latex(n2)}$ are {ns_name} numbers.

        $\\displaystyle {n1_latex} {op_sym} {n2_latex}
        = {latex(n1n2)}$

        $\\displaystyle {n2_latex} {op_sym} {n1_latex}
        = {latex(n2n1)}$


        $\\displaystyle \\implies
        {n1_latex} {op_sym} {n2_latex}
        {is_true_stmt} {n2_latex} {op_sym} {n1_latex}$


        """
        return statement
        pass

    def get_distributivity_statement(self, syms, nums, ns_name, op_details, is_true):
        sOut = f"""
        distributivity:
        ns_name = {ns_name}
        nums = {nums}
        is_true == {is_true}
        op_details = {op_details}
        """
        #print(sOut)

        a, b, c = syms
        n1, n2, n3 = nums

        n1_latex = self.get_var_latex(n1)
        n2_latex = self.get_var_latex(n2)
        n3_latex = self.get_var_latex(n3)

        op_sym = op_details[2]
        op = op_details[0]

        n1_n2n3 = simplify(op(n1, n2 + n3))
        n1n2_n1n3 = simplify(op(n1, n2) + op(n1, n3))

        if self._are_equal(n1_n2n3, n1n2_n1n3) != is_true:
            return None

        if is_true:
            is_true_stmt = "="
        else:
            is_true_stmt = f"\\neq"

        statement = f"""
        if $\\displaystyle {latex(a)}$, $\\displaystyle {latex(b)}$ and $\\displaystyle {latex(c)}$ are {ns_name} numbers, then
        $\\displaystyle {latex(a)} {op_sym} ({latex(b)} + {latex(c)} )
        = ({latex(a)} {op_sym} {latex(b)}) + ({latex(a)} {op_sym} {latex(c)}) $

        Example:

        $\\displaystyle {latex(n1)}$, $\\displaystyle {latex(n2)}$ and $\\displaystyle {latex(n3)}$ are {ns_name} numbers.

        $\\displaystyle {n1_latex} {op_sym} \\left({n2_latex} + {n3_latex} \\right)
        = {latex(n1_n2n3)}$

        $\\displaystyle \\left({n1_latex} {op_sym} {n2_latex}\\right) + ({n1_latex} {op_sym} {n3_latex})
        = {latex(n1n2_n1n3)}$

        $\\displaystyle \\implies
        {n1_latex} {op_sym} \\left({n2_latex} + {n3_latex}\\right)
        {is_true_stmt}
        \\left({n1_latex} {op_sym} {n2_latex}\\right)
        + \\left({n1_latex} {op_sym} {n3_latex}\\right)$

        """
        return statement
        pass


    def get_sys_prop_rule_statement(self,
                                   nums,
                                   ns_name,
                                   prop_name,
                                   op):


        pass

    def get_ns_props(self,
                     ns_name = None,
                     prop_name = None,
                     op = None
                     ):
        props = self.get_all_ns_props()
        if ns_name is None:
            return props

        props = props.get(ns_name, None)
        if props is None:
            return None

        if prop_name is None:
            return props
            pass

        props = props.get(prop_name, None)
        if props is None:
            return None

        if op is None:
            return props
            pass

        #if hasattr(op, '__name__'):
        #    op = op.__name__
        op = self._op_to_str(op)

        return props.get(op, None)
        pass

    def get_prop_names(self):
        return ['closure', 'commutative', 'associative', 'distributivity']
        pass


    def get_ns_names(self, is_advanced = False):
        if is_advanced:
            return ['real', 'complex']
        return ['natural', 'whole', 'integer', 'rational']
        pass

    def get_ns_types(self, n, is_advanced = False):
        ns_types = {'natural': self.is_integer(n) and n > 0,
                    'whole': self.is_integer(n) and n >= 0,
                    'integer': self.is_integer(n),
                    'rational': self.is_rational(n)
                    }
        if is_advanced:
            ns_types['real'] = self.is_real(n)
            ns_types['complex'] = self.is_complex(n)
        return ns_types
        pass




    def get_number_with_types(self, is_advanced = False):
        n_natural = random.randint(1, 1000)
        n_whole = random.choice([0, n_natural])
        n_integer = random.choice([0, n_natural, -n_natural])
        n_rational = Rational(n_integer, random.choice([random.randint(1, 1000), 1]))
        n_real = n_whole ** Rational(1, random.randint(1, 4))
        n_complex = (- abs(n_rational)) ** Rational(1, random.randint(1, 4))

        nums = [n_natural, n_whole, n_integer, n_rational]
        if is_advanced:
            nums.extend([n_real, n_complex])

        n_selected = sympify(random.choice(nums))
        num_types_kv = self.get_ns_types(n_selected, is_advanced)

        return n_selected, num_types_kv

    def get_random_number(self, is_advanced = False):
        ns_names = self.get_ns_names(is_advanced)

        ops = [(operator.add, 'addition', "+"),
               (operator.mul, 'multiplication', "*")]

        ns_name = random.choice(ns_names)
        op = random.choice(ops)

        method_name = f"get_{ns_name}_numbers"
        if not hasattr(self, method_name):
            raise Exception(f"Invalid ns_name: {ns_name}")

        method = getattr(self, method_name)
        num = method(1)[0]
        x = symbols('x')
        if op[0] == operator.add:
            eq = Eq(num + x, 0)
            expr_latex = f"{latex(num)} + {latex(x)} = 0"
        else:
            eq = Eq(num * x, 1)
            expr_latex = f"{latex(num)} * {latex(x)} = 1"

        x_vals = solve(eq, x)
        if not x_vals:
            x_vals = None

        return (ns_name, op, num, x, eq, expr_latex, x_vals)



    def get_closure_properties(self, is_advanced = True):
        ns_names = self.get_ns_names(is_advanced)

        ops = [(operator.add, 'addition', "+"),
               (operator.sub, 'subtraction', "-"),
               (operator.mul, 'multiplication', "*"),
               (operator.truediv, 'division', "/")]

        closure_properties = {}
        for ns_name in ns_names:
            closure_properties[ns_name] = {}
            for op, op_desc, op_symbol in ops:
                result = self.is_closure(ns_name, op, op_symbol)
                result['op_desc'] = op_desc
                result['op_symbol'] = op_symbol
                closure_properties[ns_name][op.__name__] = result
                pass
            pass
        return closure_properties
        pass

    def get_commutative_properties(self, is_advanced = True):
        ns_names = self.get_ns_names(is_advanced)

        ops = [(operator.add, 'addition', "+"),
               (operator.mul, 'multiplication', "*")]

        commutative_properties = {}
        for ns_name in ns_names:
            commutative_properties[ns_name] = {}
            for op, op_desc, op_symbol in ops:
                result = self.is_commutative(ns_name, op, op_symbol)
                result['op_desc'] = op_desc
                result['op_symbol'] = op_symbol
                commutative_properties[ns_name][op.__name__] = result
                pass
            pass
        return commutative_properties
        pass


    def get_associative_properties(self, is_advanced = True):
        ns_names = self.get_ns_names(is_advanced)


        ops = [(operator.add, 'addition', "+"),
               (operator.mul, 'multiplication', "*")]

        associative_properties = {}
        for ns_name in ns_names:
            associative_properties[ns_name] = {}
            for op, op_desc, op_symbol in ops:
                result = self.is_associative(ns_name, op, op_symbol)
                result['op_desc'] = op_desc
                result['op_symbol'] = op_symbol
                associative_properties[ns_name][op.__name__] = result
                pass
            pass
        return associative_properties
        pass

    def get_distributivity_properties(self, is_advanced = True):
        ns_names = self.get_ns_names(is_advanced)


        ops = [(operator.mul, 'multiplication', "*")]

        distributivity_properties = {}
        for ns_name in ns_names:
            distributivity_properties[ns_name] = {}
            for op, op_desc, op_symbol in ops:
                result = self.is_distributivity(ns_name, op, op_symbol)
                result['op_desc'] = op_desc
                result['op_symbol'] = op_symbol
                distributivity_properties[ns_name][op.__name__] = result
                pass
            pass
        return distributivity_properties
        pass

    def is_closure(self, ns_name, op, op_symbol):
        try:
            method_name = f"get_{ns_name}_numbers"
            if not hasattr(self, method_name):
                raise Exception(f"Invalid ns_name: {ns_name}")

            method = getattr(self, method_name)
            max_loop_count = 100
            loop_count = 0
            is_true = True
            while loop_count < max_loop_count:
                loop_count = loop_count + 1
                n1, n2 = method(2)
                if n1 is None or n2 is None:
                    continue
                if n1 == sympy.nan or n2 == sympy.nan:
                    continue


                test_method_name = f"is_{ns_name}"

                if not hasattr(self, test_method_name):
                    raise Exception(f"Invalid test_method_name: {test_method_name}")

                test_method = getattr(self, test_method_name)
                n1n2 = op(n1, n2)
                is_closure = test_method(n1) and test_method(n2) and test_method(n1n2)
                if not is_closure:
                    is_true = False
                    break
                pass

            result = {'is_true': is_true,
                      'n1': n1, 'n2': n2,
                      'n1n2': n1n2,
                      'n1n2_latex': f"{latex(n1)} {op_symbol} {latex(n2)}"}
            return result
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:
            {e}"""
            #print(sError)
            #print(traceback.format_exc())
            return {'is_true': False}

    def is_commutative(self, ns_name, op, op_symbol):
        try:
            method_name = f"get_{ns_name}_numbers"
            if not hasattr(self, method_name):
                raise Exception(f"Invalid ns_name: {ns_name}")

            method = getattr(self, method_name)
            max_loop_count = 100
            loop_count = 0
            is_true = True
            while loop_count < max_loop_count:
                loop_count = loop_count + 1
                n1, n2 = method(2)
                if n1 is None or n2 is None:
                    continue
                if n1 == sympy.nan or n2 == sympy.nan:
                    continue


                test_method_name = f"is_{ns_name}"

                if not hasattr(self, test_method_name):
                    raise Exception(f"Invalid test_method_name: {test_method_name}")

                n1n2 = op(n1, n2)
                n2n1 = op(n2, n1)

                if not self._are_equal(n1n2, n2n1):
                    is_true = False
                    break
                pass

            result = {'is_true': is_true,
                      'n1': n1, 'n2': n2,
                      'n1n2': n1n2,
                      'n2n1': n2n1,
                      'n1n2_latex': f"{latex(n1)} {op_symbol} {latex(n2)}",
                      'n2n1_latex': f"{latex(n2)} {op_symbol} {latex(n1)}"
                      }
            return result
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:
            {e}"""
            #print(sError)
            #print(traceback.format_exc())
            return {'is_true': False}

    def is_associative(self, ns_name, op, op_symbol):
        try:
            method_name = f"get_{ns_name}_numbers"
            if not hasattr(self, method_name):
                raise Exception(f"Invalid ns_name: {ns_name}")

            method = getattr(self, method_name)
            max_loop_count = 100
            loop_count = 0
            is_true = True
            while loop_count < max_loop_count:
                loop_count = loop_count + 1
                n1, n2, n3 = method(3)
                if n1 is None or n2 is None or n3 is None:
                    continue
                if n1 == sympy.nan or n2 == sympy.nan or n3 == sympy.nan:
                    continue


                test_method_name = f"is_{ns_name}"

                if not hasattr(self, test_method_name):
                    raise Exception(f"Invalid test_method_name: {test_method_name}")

                n1n2_n3 = op(op(n1, n2), n3)
                n1_n2n3 = op(n1, op(n2, n3))

                if n1n2_n3 != n1_n2n3:
                    is_true = False
                    break
                pass

            result = {'is_true': is_true,
                      'n1': n1, 'n2': n2, 'n3': n3,
                      'n1n2_n3': n1n2_n3,
                      'n1_n2n3': n1_n2n3,
                      'n1n2_n3_latex': f"({latex(n1)} {op_symbol} {latex(n2)}) {op_symbol} {latex(n3)}",
                      'n1_n2n3_latex': f"{latex(n1)} {op_symbol} ({latex(n2)} {op_symbol} {latex(n3)})"
                      }
            return result
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:
            {e}"""
            #print(sError)
            #print(traceback.format_exc())
            return {'is_true': False}

    def is_distributivity(self, ns_name, op, op_symbol):
        try:
            method_name = f"get_{ns_name}_numbers"
            if not hasattr(self, method_name):
                raise Exception(f"Invalid ns_name: {ns_name}")

            method = getattr(self, method_name)
            max_loop_count = 100
            loop_count = 0
            is_true = True
            while loop_count < max_loop_count:
                loop_count = loop_count + 1
                n1, n2, n3 = method(3)
                if n1 is None or n2 is None or n3 is None:
                    continue
                if n1 == sympy.nan or n2 == sympy.nan or n3 == sympy.nan:
                    continue


                test_method_name = f"is_{ns_name}"

                if not hasattr(self, test_method_name):
                    raise Exception(f"Invalid test_method_name: {test_method_name}")


                n1_n2n3 = op(n1, n2 + n3)
                n1n2_n1n3 = op(n1, n2) + op(n1, n3)

                if not self._are_equal(n1_n2n3, n1n2_n1n3):
                    is_true = False
                    break

                pass

            result = {'is_true': is_true,
                      'n1': n1, 'n2': n2, 'n3': n3,
                      'n1_n2n3': n1_n2n3,
                      'n1n2_n1n3': n1n2_n1n3,
                      'n1_n2n3_latex': f"{latex(n1)} {op_symbol} ({latex(n2)} + {latex(n3)})",
                      'n1n2_n1n3_latex': f"({latex(n1)} {op_symbol} {latex(n2)}) + ({latex(n1)} {op_symbol} {latex(n3)})",
                      }
            return result
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:
            {e}"""
            #print(sError)
            #print(traceback.format_exc())
            return {'is_true': False}


    def is_natural(self, n):
        if not self.is_integer(n):
            return False
        return n > 0

    def is_whole(self, n):
        if not self.is_integer(n):
            return False
        return n >= 0

    def is_integer(self, n):
        try:
            return int(n) == n
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:
            {e}"""
            #print(sError)
            #print(traceback.format_exc())
            try:
                return sympify(n).is_integer
            except:
                return False

    def is_rational(self, n):
        return sympify(n).is_rational

    def is_irrational(self, n):
        return sympify(n).is_real and not sympify(n).is_rational

    def is_real(self, n):
        return sympify(n).is_real

    def is_complex(self, n):
        return sympify(n).is_complex


    def get_natural_numbers(self, count = 2, low = 1, high = 9, simple = False):
        nums = []
        while len(nums) < count:
            temp = random.randint(low, high)
            if not temp in nums:
                nums.append(temp)
            pass
        return nums

    def get_whole_numbers(self, count = 2, low = 0, high = 9, simple = False):
        nums = []
        while len(nums) < count:
            temp = random.choice([random.randint(low, high), 0])
            if not temp in nums:
                nums.append(temp)
            pass
        return nums

    def get_integer_numbers(self, count = 2, low = -9, high = 9, simple = False):
        nums = []
        while len(nums) < count:
            t1 = self.get_whole_numbers(count, low, high, simple = simple)[0]
            temp = random.choice([t1, -t1])
            if not temp in nums:
                nums.append(temp)
            pass
        return nums

    def get_rational_numbers(self, count = 2, low = -9, high = 9, simple = False):
        nums = []
        while len(nums) < count:
            t1 = self.get_integer_numbers(1, low, high, simple = simple)[0]
            t1 = random.choice([0, 1, t1])

            t2 = self.get_natural_numbers(1, low, high, simple = simple)[0]
            t2 = random.choice([1, t2])
            temp = Rational(t1, t2)

            if temp is None:
                continue
            if not temp in nums:
                nums.append(temp)
            pass
        return nums

    def get_irrational_numbers(self, count = 2, simple = False):
        temp_nums = [2 ** (1/2), 3 ** (1/2), pi, E]
        nums = []
        while len(nums) < count:
            temp = random.randint(1, 9) * random.choice(temp_nums)
            if temp is None:
                continue
            if not temp in nums:
                nums.append(temp)
            pass
        return nums

    def get_real_numbers(self, count = 2, low = -9, high = 9, simple = False):
        nums = []
        while len(nums) < count:
            t1 = self.get_rational_numbers(count, low, high, simple = simple)[0]
            t2 = self.get_irrational_numbers(count, simple = simple)[0]
            temp = random.choice([t1, t2])
            if temp is None:
                continue
            if not temp in nums:
                nums.append(temp)
            pass
        return nums

    def get_complex_numbers(self, count = 2, low = -9, high = 9, simple = False):
        nums = []
        while len(nums) < count:
            if simple:
                temp1 = self.get_natural_numbers(1, 1, high, simple = simple)[0]
                temp2 = self.get_natural_numbers(1, 0, high, simple = simple)[0]
                temp = temp1 * I + temp2
            else:
                temp = self.get_real_numbers(1, low, high)[0]
                if temp is None:
                    continue
                temp = temp * I

            if not temp in nums:
                nums.append(temp)
            pass
        return nums

    def _are_equal(self, n1, n2):
        try:
            max_error = 10 ** (-4)
            if not sympify(n1).is_complex or not sympify(n2).is_complex:
                return abs(simplify(n1) - simplify(n2)) < max_error

            n1r, n1i = sympify(n1).as_real_imag()
            n2r, n2i = sympify(n2).as_real_imag()

            return (abs(simplify(n1r) - simplify(n2r)) < max_error and
                    abs(simplify(n1i) - simplify(n2i)) < max_error)

        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:
            {e}"""
            print(sError)
            print(traceback.format_exc())
            return False
        pass

    def get_var_latex(self, v):
        if len(f"{v}") == 1:
            v_latex = f"{latex(v)}"
            return v_latex

        v_latex = f"\\left({latex(v)}\\right)"
        if isinstance(v, int):
            if v > 0:
                v_latex = f"{latex(v)}"
            pass

        return v_latex

    def get_ops(self,
                ns_name = None,
                prop_name = None,
                details = False):

        ops = [(operator.add, 'addition', "+"),
               (operator.sub, 'subtraction', "-"),
               (operator.mul, 'multiplication', "*"),
               (operator.truediv, 'division', "/")]

        if (not ns_name is None and
            not prop_name is None):
            ns_props = self.get_ns_props(ns_name, prop_name)
            ops = [op for op in ops
                   if op[0].__name__ in ns_props.keys()
                   ]
            pass

        if details:
            return ops
        ops = [op[0] for op in ops]
        return ops

    def get_op_details(self, op):
        for item in self.get_ops(details = True):
            if item[0] == op or item[0].__name__ == op:
                return item
            pass
        return None
        pass

    def _op_to_str(self, op):
        if hasattr(op, '__name__'):
            op = op.__name__
        return f"{op}"



    def get_all_ns_props(self):
        props = {'natural': {'closure': {'add': True,
                   'sub': False,
                   'mul': True,
                   'truediv': False},
                  'commutative': {'add': True, 'mul': True},
                  'associative': {'add': True, 'mul': True},
                  'distributivity': {'mul': True}},
                 'whole': {'closure': {'add': True,
                   'sub': False,
                   'mul': True,
                   'truediv': False},
                  'commutative': {'add': True, 'mul': True},
                  'associative': {'add': True, 'mul': True},
                  'distributivity': {'mul': True}},
                 'integer': {'closure': {'add': True,
                   'sub': True,
                   'mul': True,
                   'truediv': False},
                  'commutative': {'add': True, 'mul': True},
                  'associative': {'add': True, 'mul': True},
                  'distributivity': {'mul': True}},
                 'rational': {'closure': {'add': True,
                   'sub': True,
                   'mul': True,
                   'truediv': False},
                  'commutative': {'add': True, 'mul': True},
                  'associative': {'add': True, 'mul': True},
                  'distributivity': {'mul': True}},
                 'real': {'closure': {'add': True, 'sub': True, 'mul': True,
                                      'truediv': True},
                  'commutative': {'add': True, 'mul': True},
                  'associative': {'add': True, 'mul': True},
                  'distributivity': {'mul': True}},
                 'complex': {'closure': {'add': True,
                   'sub': True,
                   'mul': True},
                  'commutative': {'add': True, 'mul': True},
                  'associative': {'add': True, 'mul': True},
                  'distributivity': {'mul': True}}}
        return props


    def get_number_system_labels(self, num = None):
        labels = self.get_default_number_system_labels()
        if num is None:
            return labels

        num = sympify(num)

        if num.is_imaginary:
            labels['Imaginary'] = labels['Imaginary'] + f"\n ${latex(num)}$"
            return labels


        if num.is_Integer:
            if num > 0:
                labels['Natural'] = labels['Natural'] + f"\n ${latex(num)}$"
                return labels

            if num == 0:
                labels['Whole'] = labels['Whole'] + f"\n ${latex(num)}$"
                return labels

            labels['Integer'] = labels['Integer'] + f"\n ${latex(num)}$"
            return labels


        if num.is_irrational:
            labels['Irrational'] = labels['Irrational'] + f"\n ${latex(num)}$"
            return labels

        if num.is_Rational:
            labels['Rational'] = labels['Rational'] + f"\n ${latex(num)}$"
            return labels

        if num.is_real:
            labels['Real'] = labels['Real'] + f"\n ${latex(num)}$"
            return labels

        if num.is_complex:
            labels['Complex'] = labels['Complex'] + f"\n ${latex(num)}$"
            return labels

        return labels

    def get_default_number_system_labels(self):
        number_system_labels = {
            'Complex': r'Complex $\mathbb{C}$',
            'Real': r'Real $\mathbb{R}$',
            'Imaginary': r'Imaginary $\mathbb{I}$',
            'Rational': r'Rational $\mathbb{Q}$',
            'Irrational': r'Irrational $\mathbb{R} - \mathbb{Q}$',
            'Integers': r'Integers ${\mathbb{Z}}$',
            'Whole': r'Whole ${\mathbb{W}}$',
            'Natural': r'Natural ${\mathbb{N}}$',
            }
        return number_system_labels

    def plot_sets_number_system(self, **kwargs):
        import matplotlib.pyplot as plt
        from xv.plotter import Plotter2D
        from xv.km.ui import SvgHelper
        import numpy as np

        num = kwargs.get('num', None)
        is_advanced = kwargs.get('is_advanced', True)
        labels = self.get_number_system_labels(num = num)

        verbose = self.verbose
        plt.ioff()

        sets = []

        r0 = 80
        c0 = np.array([0, 0])
        set_data = {
            'r': r0,
            'center': c0,
            'label': labels['Complex'],
            'label_pos': (-0.1, .90),
        }

        if is_advanced:
            sets.append(set_data)


        r1 = r0 * .75
        c1 = c0 - np.array([+r0 - r1, 0])
        set_data = {
            'r': r1,
            'center': c1,
            'label': labels['Real'],
            'label_pos': (-0.1, .90),
        }
        sets.append(set_data)

        r2 = r0 * .2
        c2 = c0 + np.array([+r0 - r2, 0])
        set_data = {
            'r': r2,
            'center': c2,
            'label': labels['Imaginary'],
            'label_pos': (-0.9, 0),
        }

        if is_advanced:
            sets.append(set_data)

        r3 = r1 * .70
        c3 = c1 - np.array([+r1 - r3, 0])

        set_data = {
            'r': r3,
            'center': c3,
            'label': labels['Rational'],
            'label_pos': (-.2, 0.8),
        }
        sets.append(set_data)


        r4 = r1 * .25
        c4 = c1 + np.array([+r1 - r4, 0])

        set_data = {
            'r': r4,
            'center': c4,
            'label': labels['Irrational'],
            'label_pos': (-0.75, 0),
        }
        sets.append(set_data)


        r5 = r3 * .8
        c5 = c3 - np.array([(+r3 - r5) * 0.5, (+r3 - r5) * 0.3])

        set_data = {
            'r': r5,
            'center': c5,
            'label': labels['Integers'],
            'label_pos': (-0.8, 0.3),
        }
        sets.append(set_data)

        r6 = r5 * .6
        c6 = c5 + np.array([+r5 - r6, 0])



        set_data = {
            'r': r6,
            'center': c6,
            'label': labels['Whole'],
            'label_pos': (-0.75, 0),
        }
        sets.append(set_data)

        r7 = r6 * .55
        c7 = c6 + np.array([+r6 - r7, 0])

        set_data = {
            'r': r7,
            'center': c7,
            'label': labels['Natural'],
            'label_pos': (-0.6, 0),
        }
        sets.append(set_data)


        plotter = Plotter2D(verbose = verbose)

        fig = plt.figure(figsize = (20, 6))

        fig, ax = plotter.plot_sets(sets, ellipse_ratio = .5, fig = fig)

        svgHelper = SvgHelper()
        svg_str = svgHelper.plt_to_svg()

        plt.clf()
        plt.close()


        return svg_str
        pass


    pass
#class NumberSystemHelper